@johnmorrisdotca/wakusei-planets 0.4.0 → 0.5.1
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.
- package/CHANGELOG.md +20 -5
- package/README.md +258 -2
- package/dist/element.d.ts +2 -2
- package/dist/scene/glsl.d.ts +10 -1
- package/dist/scene/glsl.js +36 -2
- package/dist/scene/orient.d.ts +38 -0
- package/dist/scene/orient.js +72 -0
- package/dist/scene/painter.d.ts +164 -0
- package/dist/scene/painter.js +355 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.5.1 - 2026-11-03
|
|
4
|
+
|
|
5
|
+
- kazoku-ui 0.12.0: the demo's view controls have a zoom slider, on top of 0.11's Centre button and exact grab.
|
|
6
|
+
- Documentation: an architecture guide and a fuller upgrading guide, each with its Japanese twin; a code builder page (`builder.html`); eight new pictures; a longer README whose examples run (`pnpm test:readme`); and every release of this changelog dated. No behaviour changed.
|
|
7
|
+
- The README opens with the package's thumbnail, and the demo carries it as its link preview (og:image) and publishes the card, a dark card and an icon on Pages at `thumbnail.png`, `thumbnail-dark.png` and `icon.png`.
|
|
8
|
+
|
|
9
|
+
## 0.5.0 - 2026-11-03
|
|
10
|
+
|
|
11
|
+
The bodies for someone else's three.js scene.
|
|
12
|
+
|
|
13
|
+
- **`@johnmorrisdotca/wakusei-planets/painter`**: `painter.make(three, "Jupiter", { look, invalidate, ratio })` makes the Sun, a planet, Pluto or a moon as an object of radius 1 for a perspective-camera scene of your own, in the realistic or the painted look, lit from any direction in the scene's own axes (equatorial J2000, y up), turned about its real pole at the moment given, with its rings and the Sun's glow. Nothing is drawn by it and it holds no renderer, loop or clock: the caller places, scales and calls `update({ toSun, ms, radiusPx })` at each draw. A painted body shows the shaded look until its map arrives, fetches a sharper map only when it is big on the screen, and lets the big one go a few seconds after it shrinks. `painter.moons("Jupiter", ms)` places a planet's moons in planet radii. Its shape is chikyu-earth's `BodyPainter`, so `setBodyPainter(painter)` is the whole wiring (chikyu-earth 0.11).
|
|
14
|
+
- New shaders for it (`VERTEX_WORLD` and friends): the lighting, the ring's shadow and the limb are worked out from the direction to the camera, not from an orthographic viewer's frame.
|
|
15
|
+
- `orientation(id, ms)` and `moonOffset(id, ms)`: a body's pole and prime meridian, and a moon's place from its planet, in the same axes.
|
|
16
|
+
- A guide, "In your own three.js scene", in English and Japanese.
|
|
17
|
+
|
|
18
|
+
## 0.4.0 - 2026-11-03
|
|
4
19
|
|
|
5
20
|
"Put it in your page" is a configurator.
|
|
6
21
|
|
|
@@ -9,7 +24,7 @@
|
|
|
9
24
|
- A guide, "Put it in your page", in English and Japanese.
|
|
10
25
|
- **kazoku-ui 0.11.0**: the view controls' Centre button (it appears once the body is dragged away), a pan that keeps a quarter of the body in sight (told the body's size with `object-size`), and a drag that keeps the touched point under the pointer (`grab-radius`). The 0.3.1 stopgap that made Fit re-centre is gone.
|
|
11
26
|
|
|
12
|
-
## 0.3.1
|
|
27
|
+
## 0.3.1 - 2026-11-03
|
|
13
28
|
|
|
14
29
|
The Earth is a body like the others, and "To scale" is a comparison.
|
|
15
30
|
|
|
@@ -19,7 +34,7 @@ The Earth is a body like the others, and "To scale" is a comparison.
|
|
|
19
34
|
- **The body cannot be dragged out of the view** (a quarter of it stays in), and Fit centres it and returns it to its first angle.
|
|
20
35
|
- The README's pictures are absolute addresses pinned to the release's tag (npm's page showed a different picture from GitHub's), and a test holds it (`pnpm readme:images` pins them after a version bump).
|
|
21
36
|
|
|
22
|
-
## 0.3.0
|
|
37
|
+
## 0.3.0 - 2026-11-03
|
|
23
38
|
|
|
24
39
|
Painted moons, Saturn's rings from Cassini, and a drag that goes the right way up.
|
|
25
40
|
|
|
@@ -31,7 +46,7 @@ Painted moons, Saturn's rings from Cassini, and a drag that goes the right way u
|
|
|
31
46
|
- **A drag up moves the surface up** (it moved it down), in every look: the controls' tilt tipped the pole toward you as it grew, and now tips the top of the body away, so the near surface goes up, as in chikyu-earth 0.9.1. Drag right, the right arrow and the up arrow agree. Test (`e2e/drag.demo.ts`): a real pointer and real key presses on Mars (line, shaded, painted), Venus and the Moon (painted); the vertical checks fail on 0.2.0.
|
|
32
47
|
- `scripts/make-textures.py` makes the moons and the ring strip too (`python3 scripts/make-textures.py io saturn-ring`) and writes each source's file page, licence and credit into `textures/SOURCES.json`.
|
|
33
48
|
|
|
34
|
-
## 0.2.0
|
|
49
|
+
## 0.2.0 - 2026-11-02
|
|
35
50
|
|
|
36
51
|
The painted looks.
|
|
37
52
|
|
|
@@ -39,7 +54,7 @@ The painted looks.
|
|
|
39
54
|
- Small pictures by default: the 512 and 1024 pixel widths are in the package (0.53 MB in all); a view that is zoomed in fetches the 2048 pixel width from the repository by jsDelivr's GitHub address (`texture-far-base` to serve it yourself). A picture that fails to load leaves the shaded look drawn, and no picture is asked for twice.
|
|
40
55
|
- A texture is freed when a sharper one replaces it and when the element leaves.
|
|
41
56
|
|
|
42
|
-
## 0.1.0
|
|
57
|
+
## 0.1.0 - 2026-11-03
|
|
43
58
|
|
|
44
59
|
The first release. The package, to the family's standard from the start: a demo and an embed page on GitHub Pages, a documentation site in English and Japanese, a README with every way to use it (each one tested), the shared release workflow with its freeze test.
|
|
45
60
|
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<p align="center"><a href="https://johnmorrisdotca.github.io/wakusei-planets/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/main/docs/images/thumbnail-dark.png"><img src="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/main/docs/images/thumbnail.png" alt="Wakusei Planets: Mars, the Earth, Saturn with its rings, Jupiter and Neptune, shaded in 3D, side by side." width="1200"></picture></a></p>
|
|
2
|
+
|
|
1
3
|
<h1 align="center">Wakusei Planets <sub>惑星</sub></h1>
|
|
2
4
|
|
|
3
5
|
<p align="center"><strong>The Sun, the planets and their moons, for the web.</strong><br>
|
|
@@ -11,9 +13,9 @@ A clean line drawing, a shaded realistic drawing, or a painted surface map, with
|
|
|
11
13
|
|
|
12
14
|
<p align="center"><a href="https://johnmorrisdotca.github.io/wakusei-planets/"><strong>Open the demo →</strong></a> · <a href="https://johnmorrisdotca.github.io/wakusei-planets/embed.html">Embed page</a> · <a href="https://johnmorrisdotca.github.io/wakusei-planets/docs/">Docs</a></p>
|
|
13
15
|
|
|
14
|
-
<p align="center"><img alt="Jupiter and Saturn drawn in the realistic look, and Saturn as a line drawing" src="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/v0.
|
|
16
|
+
<p align="center"><img alt="Jupiter and Saturn drawn in the realistic look, and Saturn as a line drawing" src="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/v0.5.1/docs/images/hero-desk.png" width="640"></p>
|
|
15
17
|
|
|
16
|
-
<p align="center"><img alt="Every body at one scale, the Earth among them: each small body has a magnified picture joined by a line, and the Sun shows only its edge" src="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/v0.
|
|
18
|
+
<p align="center"><img alt="Every body at one scale, the Earth among them: each small body has a magnified picture joined by a line, and the Sun shows only its edge" src="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/v0.5.1/docs/images/lineup-desk.png" width="640"></p>
|
|
17
19
|
|
|
18
20
|
## One minute
|
|
19
21
|
|
|
@@ -68,6 +70,37 @@ The same tag with the shared [kazoku-ui](https://github.com/johnmorrisdotca/kazo
|
|
|
68
70
|
|
|
69
71
|
A bundled app imports it: `import "@johnmorrisdotca/wakusei-planets/element"` (and `import { lineSvg } from "@johnmorrisdotca/wakusei-planets"` for the SVG string, which needs no browser). For the shaded look add the body's own entry point, `import "@johnmorrisdotca/wakusei-planets/jupiter"`, and `three` (an optional peer).
|
|
70
72
|
|
|
73
|
+
## Who it is for
|
|
74
|
+
|
|
75
|
+
- **A developer who wants a planet on a page** with one tag, no build step, and a drawing that is an SVG before any 3D has loaded.
|
|
76
|
+
- **A teacher, writer or museum** who needs the Sun, the planets and their moons at their real sizes and tilts, with every number traceable to the IAU, NASA or JPL.
|
|
77
|
+
- **A maker of sky tools** who wants a planet's pole, phase, ring opening and its moons' places for any date and time as plain data, on a server or in a browser.
|
|
78
|
+
- **A three.js developer** who wants the bodies as objects for a scene of their own.
|
|
79
|
+
|
|
80
|
+
## Features
|
|
81
|
+
|
|
82
|
+
- The Sun, the eight planets, Pluto and 25 moons, each with its real size, flattening, axial tilt, pole and rotation as data.
|
|
83
|
+
- Three looks: a clean `line` drawing (an SVG string), a shaded `realistic` sphere worked out in a shader, and a `painted` surface map from public-domain imagery.
|
|
84
|
+
- The sky for any date and time: a body's pole, rotation, phase and ring opening as the Earth sees it, and a planet's moons where they are then.
|
|
85
|
+
- One WebGL context for the page, nothing drawn at rest, at most thirty frames a second, and everything freed when an element leaves.
|
|
86
|
+
- A pure side that runs on a server: `BODIES`, `apparent`, `system`, `orbitPath`, `portrait` and `lineSvg`.
|
|
87
|
+
- `embedCode` writes the code that puts a body in a page for HTML, a module, React, Vue, Angular, Svelte or an iframe.
|
|
88
|
+
- `painter.make` hands a body to a three.js scene of your own.
|
|
89
|
+
- Every guide in English and Japanese; the demo's words too.
|
|
90
|
+
|
|
91
|
+
## Use it in your project
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
npm install @johnmorrisdotca/wakusei-planets three
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```ts no-run
|
|
98
|
+
import "@johnmorrisdotca/wakusei-planets/element";
|
|
99
|
+
import "@johnmorrisdotca/wakusei-planets/saturn";
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Import the element once in client code, and the entry point of each body you draw in a shaded look (or `/all`). A line drawing needs neither `three` nor an entry point. [Bundlers](docs/guides/bundlers.md) and [Frameworks](docs/guides/frameworks.md) go through it.
|
|
103
|
+
|
|
71
104
|
## Three looks
|
|
72
105
|
|
|
73
106
|
| Look | What it is | Needs |
|
|
@@ -145,6 +178,229 @@ The pure side has no DOM: `BODIES`, `apparent(id, time)`, `system(planet, time)`
|
|
|
145
178
|
|
|
146
179
|
Sizes, flattening, poles and rotation are the IAU's and NASA's; planet positions are JPL's approximate elements; the Moon's are Meeus's largest terms; the other moons are JPL's mean elements. Nothing is fetched at run time by the line and shaded looks, which use no pictures. See [NOTICE.md](NOTICE.md).
|
|
147
180
|
|
|
181
|
+
## Examples
|
|
182
|
+
|
|
183
|
+
Every `ts` block in this README is type-checked and run against the package as npm packs it (`pnpm test:readme`).
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { BODIES, MOONS_OF, PLANET_IDS } from "@johnmorrisdotca/wakusei-planets";
|
|
187
|
+
|
|
188
|
+
BODIES.saturn.radiusKm; // 60268
|
|
189
|
+
PLANET_IDS.length; // 9
|
|
190
|
+
Object.values(MOONS_OF).flat().length; // 25
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The planets' state as the Earth sees them on a date:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { apparent } from "@johnmorrisdotca/wakusei-planets";
|
|
197
|
+
|
|
198
|
+
const saturn = apparent("saturn", Date.UTC(2026, 10, 2));
|
|
199
|
+
console.log(saturn.subObserverLat.toFixed(1), saturn.illumination.toFixed(2));
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
A planet's moons where they are on a date, as offsets from it in the planet's radii:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { system } from "@johnmorrisdotca/wakusei-planets";
|
|
206
|
+
|
|
207
|
+
const { moons } = system("jupiter", Date.UTC(2026, 10, 2));
|
|
208
|
+
console.log(moons.map((moon) => `${moon.id} ${moon.x.toFixed(1)}`).join(", "));
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
A line drawing as an SVG string, with no browser:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { lineSvg } from "@johnmorrisdotca/wakusei-planets";
|
|
215
|
+
|
|
216
|
+
const svg = lineSvg("mars", { size: 200, tilt: 15, lang: "ja" });
|
|
217
|
+
if (!svg.startsWith("<svg") || !svg.includes("火星")) throw new Error("not a Mars drawing");
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The code that puts a body in a page, for any of the ways a page is made:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { EMBED_KINDS, embedCode } from "@johnmorrisdotca/wakusei-planets";
|
|
224
|
+
|
|
225
|
+
for (const kind of EMBED_KINDS) console.log(kind, embedCode({ body: "mars", look: "line", size: 240 }, kind).split("\n")[0]);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
A body for your own three.js scene, in the browser:
|
|
229
|
+
|
|
230
|
+
The moons' places on an orbit, as points for a line you draw yourself:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { orbitPath } from "@johnmorrisdotca/wakusei-planets";
|
|
234
|
+
|
|
235
|
+
const path = orbitPath("io", Date.UTC(2026, 10, 2), 8);
|
|
236
|
+
console.log(path.length / 3, "points");
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
How much of the Moon is lit on a date:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { apparent } from "@johnmorrisdotca/wakusei-planets";
|
|
243
|
+
|
|
244
|
+
const moon = apparent("moon", Date.UTC(2026, 10, 24, 14));
|
|
245
|
+
if (moon.illumination < 0.9) throw new Error("the Moon is nearly full on 24 November 2026");
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
A page that cannot run script uses an iframe of the embed page:
|
|
249
|
+
|
|
250
|
+
```html
|
|
251
|
+
<iframe src="https://johnmorrisdotca.github.io/wakusei-planets/embed.html?body=mars&look=line" title="Mars" width="320" height="320" style="border:0"></iframe>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```ts no-run
|
|
255
|
+
import * as THREE from "three";
|
|
256
|
+
import { painter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
257
|
+
|
|
258
|
+
const scene = new THREE.Scene();
|
|
259
|
+
const jupiter = painter.make(THREE, "Jupiter", { look: "realistic", invalidate: () => {}, ratio: 1 });
|
|
260
|
+
scene.add(jupiter.object);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A body that turns by itself and a moving sky, in HTML:
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<wakusei-body body="jupiter" look="line" moons time="now" rate="600" size="360"></wakusei-body>
|
|
267
|
+
<wakusei-body body="venus" look="realistic" time="2026-11-02T12:00:00Z" turn="12" live="hover"></wakusei-body>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## The code builder
|
|
271
|
+
|
|
272
|
+
[The code builder](https://johnmorrisdotca.github.io/wakusei-planets/builder.html) is one screen: choose which planet's system, the body, the look, the size, the sky and the turning, then pick how your page is made and copy the code. It is built from `embedCode`, so it writes what the package writes. [Put it in your page](docs/guides/put-it-in-your-page.md) is its guide.
|
|
273
|
+
|
|
274
|
+
## API
|
|
275
|
+
|
|
276
|
+
The reference is a site, made from the source's own doc comments: every export has a summary, its parameters, what it returns and an example that runs when the site is built. [Open the reference](https://johnmorrisdotca.github.io/wakusei-planets/docs/).
|
|
277
|
+
|
|
278
|
+
| Entry point | What it gives |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| `@johnmorrisdotca/wakusei-planets` | The pure side: `BODIES`, `apparent`, `system`, `orbitPath`, `portrait`, `lineSvg`, `embedCode`. No browser. |
|
|
281
|
+
| `.../element` | `<wakusei-body>`; three.js is loaded only for a shaded look. |
|
|
282
|
+
| `.../look` | What a body needs to be drawn with three.js. |
|
|
283
|
+
| `.../painter` | The bodies as objects for a scene of your own. |
|
|
284
|
+
| `.../all` | Every body's shaded look, at once. |
|
|
285
|
+
| `.../saturn`, `.../jupiter` and the rest | One body's shaded look. `.../moons-jupiter` and its siblings: a planet's moons. |
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import * as pure from "@johnmorrisdotca/wakusei-planets";
|
|
289
|
+
|
|
290
|
+
console.log(Object.keys(pure).sort().join(", "));
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Guides
|
|
294
|
+
|
|
295
|
+
Each guide has a Japanese twin in [docs/guides/ja](docs/guides/ja/getting-started.md).
|
|
296
|
+
|
|
297
|
+
| Guide | What it is for |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| [Getting started](docs/guides/getting-started.md) | A body on a page |
|
|
300
|
+
| [HTML and CDN](docs/guides/html-and-cdn.md) | A script tag from jsDelivr or unpkg |
|
|
301
|
+
| [Bundlers](docs/guides/bundlers.md) | Vite and webpack, only the bodies you need |
|
|
302
|
+
| [Frameworks](docs/guides/frameworks.md) | React, Vue, Svelte, Angular |
|
|
303
|
+
| [Embed in an iframe](docs/guides/embed-in-an-iframe.md) | No script of your own |
|
|
304
|
+
| [Put it in your page](docs/guides/put-it-in-your-page.md) | Set up a body and copy the code |
|
|
305
|
+
| [Looks and bodies](docs/guides/looks-and-bodies.md) | The looks and each body's entry point |
|
|
306
|
+
| [Moons and time](docs/guides/moons-and-time.md) | The sky on a date, time that runs fast |
|
|
307
|
+
| [In your own three.js scene](docs/guides/in-your-own-scene.md) | `painter.make` |
|
|
308
|
+
| [TypeScript and Node](docs/guides/typescript-and-node.md) | Types and the pure side |
|
|
309
|
+
| [How it works](docs/guides/how-it-works.md) | Where the numbers come from |
|
|
310
|
+
| [Architecture](docs/guides/architecture.md) | How the layers fit |
|
|
311
|
+
| [Troubleshooting](docs/guides/troubleshooting.md) | Something does not appear |
|
|
312
|
+
| [Upgrading](docs/guides/upgrading.md) | What to act on between versions |
|
|
313
|
+
|
|
314
|
+
## Limits
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { BODIES } from "@johnmorrisdotca/wakusei-planets";
|
|
318
|
+
|
|
319
|
+
BODIES.pluto.kind; // "dwarf-planet"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
- A fact with no source is left out, never guessed. Moon positions are JPL's mean elements and are accurate to a few degrees on the orbit, about the size of the moon on screen.
|
|
323
|
+
- The shaded `realistic` look works out a surface from the body's real colours and features in a shader: it is a likeness, not a map. The `painted` look is a real map where one exists; the Sun, Saturn, Uranus and Neptune have no global map, so theirs are made from one photograph each, as `NOTICE.md` says.
|
|
324
|
+
- A shaded look needs WebGL. Without it the line drawing is drawn.
|
|
325
|
+
- Saturn and Uranus have their rings drawn.
|
|
326
|
+
- Pictures for the painted look are 1.31 MB in the package at 512 and 1024 pixels; a sharper picture is fetched when you zoom in.
|
|
327
|
+
|
|
328
|
+
## Accessibility
|
|
329
|
+
|
|
330
|
+
- Every `<wakusei-body>` is an image with an accessible name from its body and look, such as "Saturn, line drawing", or the Japanese name with `lang="ja"`.
|
|
331
|
+
- A reader who prefers reduced motion gets no turning and no moving sky.
|
|
332
|
+
- A body can be turned, tipped, zoomed and panned with the keyboard, and the view controls are at least 48 pixels.
|
|
333
|
+
- Names and labels are in English and Japanese (`lang="ja"`).
|
|
334
|
+
|
|
335
|
+
```html
|
|
336
|
+
<wakusei-body body="saturn" look="line" lang="ja" size="320"></wakusei-body>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Browser and runtime support
|
|
340
|
+
|
|
341
|
+
- The package needs Node 24 or later. The pure side runs in Node and in any current browser; the element needs a current browser.
|
|
342
|
+
- The shaded looks need WebGL and `three` `>=0.160.0` (an optional peer).
|
|
343
|
+
- The tests run the demo in Chromium and WebKit, at a phone's width and a desk's, and draw WebGL in software.
|
|
344
|
+
|
|
345
|
+
## Languages
|
|
346
|
+
|
|
347
|
+
The names and labels in the element are in English and Japanese (`lang`), and so are the demo's words and every guide: each guide in [docs/guides](docs/guides) has a twin in [docs/guides/ja](docs/guides/ja/getting-started.md), one language to a page.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { lineSvg } from "@johnmorrisdotca/wakusei-planets";
|
|
351
|
+
|
|
352
|
+
console.log(lineSvg("saturn", { size: 160, lang: "ja" }).includes("土星"));
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Japanese names and labels need only the `lang` attribute; the SVG string takes the same option.
|
|
356
|
+
|
|
357
|
+
## Architecture
|
|
358
|
+
|
|
359
|
+
The lowest layers are pure and import with no browser: the facts, the positions, the pose and the line drawing. The shaded looks are one entry point for each body, registered by name, and every `<wakusei-body>` shares one WebGL context through kazoku-ui. [Architecture](docs/guides/architecture.md) has the layers.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { portrait } from "@johnmorrisdotca/wakusei-planets";
|
|
363
|
+
|
|
364
|
+
const pose = portrait("uranus");
|
|
365
|
+
console.log(pose.pole.length);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Development
|
|
369
|
+
|
|
370
|
+
```sh
|
|
371
|
+
pnpm install
|
|
372
|
+
pnpm check # lint, types, unit tests, the build and the packed package
|
|
373
|
+
pnpm test:readme # every ts block of the README, type-checked and run against the packed package
|
|
374
|
+
pnpm test:usage # every way of using it in the README and the guides, built and opened
|
|
375
|
+
pnpm docs:check # the reference site's checks
|
|
376
|
+
pnpm test:demo # the demo and the docs site in a real browser
|
|
377
|
+
pnpm dev # the demo on http://127.0.0.1:8300/wakusei-planets/
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The pictures in `docs/images` are made by the package's own script:
|
|
381
|
+
|
|
382
|
+
```sh
|
|
383
|
+
ONLY=hero-saturn-painted,builder-desk pnpm screenshots
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Contributing
|
|
387
|
+
|
|
388
|
+
Work in a git worktree from `origin/main`, one branch per change:
|
|
389
|
+
|
|
390
|
+
```sh
|
|
391
|
+
git worktree add ../wakusei-work -b my-change origin/main
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The rules that matter most: draw from public facts and cite them, pictures only from public-domain sources, a fact with no source is left out, and performance is not optional.
|
|
395
|
+
|
|
396
|
+
## Changes
|
|
397
|
+
|
|
398
|
+
[CHANGELOG.md](CHANGELOG.md) lists every release with its date.
|
|
399
|
+
|
|
400
|
+
## Licence
|
|
401
|
+
|
|
402
|
+
MIT: see [LICENSE](LICENSE). The pictures are public-domain NASA, USGS and JPL imagery, each file credited in [NOTICE.md](NOTICE.md).
|
|
403
|
+
|
|
148
404
|
## The name
|
|
149
405
|
|
|
150
406
|
Wakusei (惑星, わくせい) is Japanese for a planet.
|
package/dist/element.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type WakuseiState = "line" | "loading" | "still" | "live" | "lost" | "una
|
|
|
25
25
|
* console.log(TEXTURE_BASE);
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
export declare const TEXTURE_BASE = "https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets@0.
|
|
28
|
+
export declare const TEXTURE_BASE = "https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets@0.5.1/dist/textures/";
|
|
29
29
|
/**
|
|
30
30
|
* Where the sharpest surface pictures are: the files of this exact version in the repository, on jsDelivr's GitHub address. The npm package holds only the two smaller widths, so a page that
|
|
31
31
|
* never zooms in never fetches them. Set `texture-far-base` on an element to serve them yourself.
|
|
@@ -37,7 +37,7 @@ export declare const TEXTURE_BASE = "https://cdn.jsdelivr.net/npm/@johnmorrisdot
|
|
|
37
37
|
* console.log(TEXTURE_FAR_BASE);
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
|
-
export declare const TEXTURE_FAR_BASE = "https://cdn.jsdelivr.net/gh/johnmorrisdotca/wakusei-planets@v0.
|
|
40
|
+
export declare const TEXTURE_FAR_BASE = "https://cdn.jsdelivr.net/gh/johnmorrisdotca/wakusei-planets@v0.5.1/textures/";
|
|
41
41
|
/**
|
|
42
42
|
* The `<wakusei-body>` custom element: a body of the solar system, drawn in a look.
|
|
43
43
|
*
|
package/dist/scene/glsl.d.ts
CHANGED
|
@@ -2,11 +2,20 @@ export declare const VERTEX = "\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying v
|
|
|
2
2
|
/** Noise, craters and the band palette, for the surface code of a body to call. */
|
|
3
3
|
export declare const PRELUDE = "\nfloat hash13(vec3 p) { p = fract(p * 0.1031); p += dot(p, p.zyx + 31.32); return fract((p.x + p.y) * p.z); }\nvec3 hash33(vec3 p) { p = fract(p * vec3(0.1031, 0.1030, 0.0973)); p += dot(p, p.yxz + 33.33); return fract((p.xxy + p.yxx) * p.zyx); }\nfloat vnoise(vec3 p) {\n vec3 i = floor(p); vec3 f = fract(p); f = f * f * (3.0 - 2.0 * f);\n return mix(mix(mix(hash13(i), hash13(i + vec3(1,0,0)), f.x), mix(hash13(i + vec3(0,1,0)), hash13(i + vec3(1,1,0)), f.x), f.y),\n mix(mix(hash13(i + vec3(0,0,1)), hash13(i + vec3(1,0,1)), f.x), mix(hash13(i + vec3(0,1,1)), hash13(i + vec3(1,1,1)), f.x), f.y), f.z);\n}\nfloat fbm(vec3 p) { float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * vnoise(p); p = p * 2.03 + vec3(1.7, 9.2, 3.1); a *= 0.5; } return s; }\n// Craters: a bowl and a raised rim in each cell that has one. Returns a height, negative in a bowl.\nfloat craters(vec3 p, float freq, float amp, float seed) {\n vec3 q = p * freq + seed; vec3 ip = floor(q); float h = 0.0;\n for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++) {\n vec3 c = ip + vec3(float(x), float(y), float(z));\n float on = step(0.52, hash13(c + 3.7));\n vec3 ctr = c + hash33(c);\n float rad = mix(0.12, 0.36, hash13(c + 17.3));\n float d = length(q - ctr) / rad;\n float bowl = d < 1.0 ? (d * d - 1.0) : 0.0;\n float rim = d >= 1.0 ? exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 : 0.0;\n h += on * amp * rad * (bowl + rim);\n }\n return h;\n}\nfloat lon(vec3 p) { return atan(p.y, p.x); }\nfloat lat(vec3 p) { return asin(clamp(p.z, -1.0, 1.0)); }\nfloat wrapPi(float a) { return a - 6.2831853 * floor((a + 3.1415927) / 6.2831853); }\nvec3 dirOf(float lonDeg, float latDeg) { float lo = radians(lonDeg); float la = radians(latDeg); return vec3(cos(la) * cos(lo), cos(la) * sin(lo), sin(la)); }\n// One big round bowl with a rim, by longitude, latitude and angular radius in degrees. Negative inside.\nfloat bowl(vec3 p, float lonDeg, float latDeg, float radDeg, float depth) {\n float d = acos(clamp(dot(p, dirOf(lonDeg, latDeg)), -1.0, 1.0)) / radians(radDeg);\n return d < 1.0 ? depth * (d * d - 1.0) : depth * exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3;\n}\n// A soft oval on the body, 1 inside and 0 outside, by longitude and latitude in degrees.\nfloat oval(vec3 p, float lon0, float lat0, float rx, float ry, float soft) {\n float dx = wrapPi(lon(p) - radians(lon0)) * cos(radians(lat0)) / radians(rx);\n float dy = (lat(p) - radians(lat0)) / radians(ry);\n return 1.0 - smoothstep(1.0 - soft, 1.0, sqrt(dx * dx + dy * dy));\n}";
|
|
4
4
|
/** The fragment shader of a body: the surface code goes between the prelude and `main`. */
|
|
5
|
-
export declare function bodyFragment(surface: string, painted: boolean): string;
|
|
5
|
+
export declare function bodyFragment(surface: string, painted: boolean, world?: boolean): string;
|
|
6
6
|
/** A painted body's surface code: the map by longitude and latitude, with the seam at 180 degrees left alone. */
|
|
7
7
|
export declare const PAINTED_SURFACE = "\nvec4 surface(vec3 p) {\n float u1 = atan(p.y, p.x) / 6.2831853 + 0.5;\n float u2 = fract(u1 + 0.5) - 0.5;\n float v = asin(clamp(p.z, -1.0, 1.0)) / 3.1415927 + 0.5;\n vec2 a = vec2(u1, v); vec2 b = vec2(u2, v);\n vec2 ax = dFdx(a); vec2 ay = dFdy(a); vec2 bx = dFdx(b); vec2 by = dFdy(b);\n bool useA = dot(ax, ax) + dot(ay, ay) <= dot(bx, bx) + dot(by, by);\n vec3 c = textureGrad(uMap, useA ? a : b, useA ? ax : bx, useA ? ay : by).rgb;\n return vec4(c, dot(c, vec3(0.3, 0.59, 0.11)));\n}";
|
|
8
8
|
export declare const RING_FRAGMENT = "\nprecision highp float;\nuniform sampler2D uRingTex;\nuniform vec2 uRingR;\nuniform vec3 uLight;\nuniform vec3 uRingN;\nvarying vec3 vL;\nvarying vec3 vW;\nvoid main() {\n float u = (length(vL.xy) - uRingR.x) / (uRingR.y - uRingR.x);\n if (u < 0.0 || u > 1.0) discard;\n vec4 t = texture2D(uRingTex, vec2(u, 0.5));\n float nl = dot(uRingN, uLight);\n float side = nl * uRingN.z;\n float bright = 0.16 + 0.84 * clamp(abs(nl) * 2.0, 0.0, 1.0);\n if (side < 0.0) bright *= 0.38;\n float b = dot(vW, uLight);\n float closest = sqrt(max(dot(vW, vW) - b * b, 0.0));\n float shadow = b < 0.0 ? smoothstep(1.02, 0.97, closest) : 0.0;\n vec3 c = pow(t.rgb, vec3(2.2)) * bright * (1.0 - 0.93 * shadow);\n gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), t.a);\n}";
|
|
9
9
|
export declare const RING_VERTEX = "\nvarying vec3 vL;\nvarying vec3 vW;\nvoid main() {\n vL = position;\n vec4 w = modelMatrix * vec4(position, 1.0);\n vW = w.xyz;\n gl_Position = projectionMatrix * viewMatrix * w;\n}";
|
|
10
|
+
/**
|
|
11
|
+
* The body's vertex shader for a perspective camera and a body anywhere in the scene: the normal and the position are in the scene's axes, the position in body radii from the body's centre
|
|
12
|
+
* (`uScale` is the body's size in scene units), and `vV` points at the camera.
|
|
13
|
+
*/
|
|
14
|
+
export declare const VERTEX_WORLD = "\nuniform float uScale;\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying vec3 vW;\nvarying vec3 vV;\nvoid main() {\n vObj = vec3(position.z, position.x, position.y);\n vec4 w = modelMatrix * vec4(position, 1.0);\n vN = normalize(transpose(inverse(mat3(modelMatrix))) * normal);\n vW = mat3(modelMatrix) * position / uScale;\n vV = cameraPosition - w.xyz;\n gl_Position = projectionMatrix * viewMatrix * w;\n}";
|
|
15
|
+
/** The ring's vertex shader for a perspective camera, with the same frame as `VERTEX_WORLD`. */
|
|
16
|
+
export declare const RING_VERTEX_WORLD = "\nuniform float uScale;\nvarying vec3 vL;\nvarying vec3 vW;\nvarying vec3 vV;\nvoid main() {\n vL = position;\n vec4 w = modelMatrix * vec4(position, 1.0);\n vW = mat3(modelMatrix) * position / uScale;\n vV = cameraPosition - w.xyz;\n gl_Position = projectionMatrix * viewMatrix * w;\n}";
|
|
17
|
+
/** The ring's fragment shader for a perspective camera: which side is seen comes from the direction to the camera. */
|
|
18
|
+
export declare const RING_FRAGMENT_WORLD: string;
|
|
10
19
|
export declare const GLOW_VERTEX = "\nvarying vec2 vP;\nvoid main() { vP = position.xy; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }";
|
|
11
20
|
export declare const GLOW_FRAGMENT = "\nprecision highp float;\nvarying vec2 vP;\nuniform vec3 uColour;\nvoid main() {\n float r = length(vP);\n if (r < 0.98) discard;\n float a = exp(-(r - 1.0) * 2.4) * 0.62;\n float streak = 0.82 + 0.18 * sin(atan(vP.y, vP.x) * 9.0 + r * 3.0);\n a *= streak * smoothstep(3.0, 1.6, r);\n gl_FragColor = vec4(uColour * a, a);\n}";
|
|
12
21
|
/** A cheap lit ball for a moon seen small beside its planet. */
|
package/dist/scene/glsl.js
CHANGED
|
@@ -51,7 +51,7 @@ float oval(vec3 p, float lon0, float lat0, float rx, float ry, float soft) {
|
|
|
51
51
|
return 1.0 - smoothstep(1.0 - soft, 1.0, sqrt(dx * dx + dy * dy));
|
|
52
52
|
}`;
|
|
53
53
|
/** The fragment shader of a body: the surface code goes between the prelude and `main`. */
|
|
54
|
-
export function bodyFragment(surface, painted) {
|
|
54
|
+
export function bodyFragment(surface, painted, world = false) {
|
|
55
55
|
return /* glsl */ `
|
|
56
56
|
precision highp float;
|
|
57
57
|
uniform vec3 uLight;
|
|
@@ -70,13 +70,14 @@ ${painted ? "uniform sampler2D uMap;" : ""}
|
|
|
70
70
|
varying vec3 vObj;
|
|
71
71
|
varying vec3 vN;
|
|
72
72
|
varying vec3 vW;
|
|
73
|
+
${world ? "varying vec3 vV;" : ""}
|
|
73
74
|
${PRELUDE}
|
|
74
75
|
${surface}
|
|
75
76
|
void main() {
|
|
76
77
|
vec3 p = normalize(vObj);
|
|
77
78
|
vec4 s = surface(p);
|
|
78
79
|
vec3 n = normalize(vN);
|
|
79
|
-
float mu = clamp(n.z, 0.0, 1.0);
|
|
80
|
+
float mu = ${world ? "clamp(dot(n, normalize(vV)), 0.0, 1.0)" : "clamp(n.z, 0.0, 1.0)"};
|
|
80
81
|
if (uEmissive > 0.5) {
|
|
81
82
|
float edge = pow(mu, uLimb);
|
|
82
83
|
vec3 c = s.rgb * mix(vec3(1.0, 0.55, 0.25), vec3(1.0), edge) * (0.55 + 0.45 * edge);
|
|
@@ -153,6 +154,39 @@ void main() {
|
|
|
153
154
|
vW = w.xyz;
|
|
154
155
|
gl_Position = projectionMatrix * viewMatrix * w;
|
|
155
156
|
}`;
|
|
157
|
+
/**
|
|
158
|
+
* The body's vertex shader for a perspective camera and a body anywhere in the scene: the normal and the position are in the scene's axes, the position in body radii from the body's centre
|
|
159
|
+
* (`uScale` is the body's size in scene units), and `vV` points at the camera.
|
|
160
|
+
*/
|
|
161
|
+
export const VERTEX_WORLD = /* glsl */ `
|
|
162
|
+
uniform float uScale;
|
|
163
|
+
varying vec3 vObj;
|
|
164
|
+
varying vec3 vN;
|
|
165
|
+
varying vec3 vW;
|
|
166
|
+
varying vec3 vV;
|
|
167
|
+
void main() {
|
|
168
|
+
vObj = vec3(position.z, position.x, position.y);
|
|
169
|
+
vec4 w = modelMatrix * vec4(position, 1.0);
|
|
170
|
+
vN = normalize(transpose(inverse(mat3(modelMatrix))) * normal);
|
|
171
|
+
vW = mat3(modelMatrix) * position / uScale;
|
|
172
|
+
vV = cameraPosition - w.xyz;
|
|
173
|
+
gl_Position = projectionMatrix * viewMatrix * w;
|
|
174
|
+
}`;
|
|
175
|
+
/** The ring's vertex shader for a perspective camera, with the same frame as `VERTEX_WORLD`. */
|
|
176
|
+
export const RING_VERTEX_WORLD = /* glsl */ `
|
|
177
|
+
uniform float uScale;
|
|
178
|
+
varying vec3 vL;
|
|
179
|
+
varying vec3 vW;
|
|
180
|
+
varying vec3 vV;
|
|
181
|
+
void main() {
|
|
182
|
+
vL = position;
|
|
183
|
+
vec4 w = modelMatrix * vec4(position, 1.0);
|
|
184
|
+
vW = mat3(modelMatrix) * position / uScale;
|
|
185
|
+
vV = cameraPosition - w.xyz;
|
|
186
|
+
gl_Position = projectionMatrix * viewMatrix * w;
|
|
187
|
+
}`;
|
|
188
|
+
/** The ring's fragment shader for a perspective camera: which side is seen comes from the direction to the camera. */
|
|
189
|
+
export const RING_FRAGMENT_WORLD = RING_FRAGMENT.replace("varying vec3 vW;", "varying vec3 vW;\nvarying vec3 vV;").replace("float side = nl * uRingN.z;", "float side = nl * dot(uRingN, normalize(vV));");
|
|
156
190
|
export const GLOW_VERTEX = /* glsl */ `
|
|
157
191
|
varying vec2 vP;
|
|
158
192
|
void main() { vP = position.xy; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type BodyId, type MoonId } from "../facts.ts";
|
|
2
|
+
import { type Vec3 } from "../vec.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Where a moon is from its planet's centre, in planet radii, in the scene's axes (x to the vernal equinox, y north).
|
|
5
|
+
*
|
|
6
|
+
* @param id - The moon's name.
|
|
7
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
8
|
+
* @returns The offset, in radii of the planet the moon goes round.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { moonOffset } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
13
|
+
*
|
|
14
|
+
* const [x, y, z] = moonOffset("io", Date.UTC(2026, 10, 2));
|
|
15
|
+
* console.log(Math.hypot(x, y, z).toFixed(1));
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function moonOffset(id: MoonId, ms: number): Vec3;
|
|
19
|
+
/**
|
|
20
|
+
* The north pole and the prime meridian of a body at a time, as unit vectors in the scene's axes (x to the vernal equinox, y north). A moon that keeps one face to its planet is
|
|
21
|
+
* given the direction to the planet as its meridian, so its near side faces the planet.
|
|
22
|
+
*
|
|
23
|
+
* @param id - The body's name.
|
|
24
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
25
|
+
* @returns The pole and the direction of longitude 0, both unit vectors.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { orientation } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
30
|
+
*
|
|
31
|
+
* const { pole } = orientation("mars", Date.UTC(2026, 10, 2));
|
|
32
|
+
* console.log(pole.map((n) => n.toFixed(2)).join(", "));
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function orientation(id: BodyId, ms: number): {
|
|
36
|
+
pole: Vec3;
|
|
37
|
+
meridian: Vec3;
|
|
38
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// How a body is turned in space at a time, in the axes of an equatorial J2000 scene with y up (x to the vernal equinox, y to the celestial north pole, z toward -y of the equator).
|
|
2
|
+
// Pure: no three.js. The painter uses it to turn each body about its real pole, and to find where a moon is round its planet.
|
|
3
|
+
import { moonVector } from "../ephemeris.js";
|
|
4
|
+
import { BODIES } from "../facts.js";
|
|
5
|
+
import { cross, dot, fromRaDec, OBLIQUITY, RAD, unit, wrap360 } from "../vec.js";
|
|
6
|
+
const J2000 = Date.UTC(2000, 0, 1, 12);
|
|
7
|
+
const DAY = 86_400_000;
|
|
8
|
+
/** An equatorial vector (x to the vernal equinox, z to the north pole) in the scene's axes. */
|
|
9
|
+
const toScene = (v) => [v[0], v[2], -v[1]];
|
|
10
|
+
/** An ecliptic J2000 vector in equatorial J2000 axes. */
|
|
11
|
+
const fromEcliptic = (v) => {
|
|
12
|
+
const c = Math.cos(OBLIQUITY * RAD);
|
|
13
|
+
const s = Math.sin(OBLIQUITY * RAD);
|
|
14
|
+
return [v[0], v[1] * c - v[2] * s, v[1] * s + v[2] * c];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Where a moon is from its planet's centre, in planet radii, in the scene's axes (x to the vernal equinox, y north).
|
|
18
|
+
*
|
|
19
|
+
* @param id - The moon's name.
|
|
20
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
21
|
+
* @returns The offset, in radii of the planet the moon goes round.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { moonOffset } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
26
|
+
*
|
|
27
|
+
* const [x, y, z] = moonOffset("io", Date.UTC(2026, 10, 2));
|
|
28
|
+
* console.log(Math.hypot(x, y, z).toFixed(1));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function moonOffset(id, ms) {
|
|
32
|
+
const facts = BODIES[id];
|
|
33
|
+
const radius = BODIES[facts.parent ?? "earth"].radiusKm;
|
|
34
|
+
const v = toScene(fromEcliptic(moonVector(id, ms)));
|
|
35
|
+
return [v[0] / radius, v[1] / radius, v[2] / radius];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The north pole and the prime meridian of a body at a time, as unit vectors in the scene's axes (x to the vernal equinox, y north). A moon that keeps one face to its planet is
|
|
39
|
+
* given the direction to the planet as its meridian, so its near side faces the planet.
|
|
40
|
+
*
|
|
41
|
+
* @param id - The body's name.
|
|
42
|
+
* @param ms - The instant, in milliseconds since 1970.
|
|
43
|
+
* @returns The pole and the direction of longitude 0, both unit vectors.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { orientation } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
48
|
+
*
|
|
49
|
+
* const { pole } = orientation("mars", Date.UTC(2026, 10, 2));
|
|
50
|
+
* console.log(pole.map((n) => n.toFixed(2)).join(", "));
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function orientation(id, ms) {
|
|
54
|
+
const facts = BODIES[id];
|
|
55
|
+
const { ra, dec } = facts.pole;
|
|
56
|
+
const pole = fromRaDec(ra, dec);
|
|
57
|
+
let meridian;
|
|
58
|
+
if (facts.synchronous === true && facts.parent !== undefined) {
|
|
59
|
+
const planetward = unit(fromEcliptic(moonVector(id, ms)).map((n) => -n));
|
|
60
|
+
const d = dot(planetward, pole);
|
|
61
|
+
meridian = unit([planetward[0] - pole[0] * d, planetward[1] - pole[1] * d, planetward[2] - pole[2] * d]);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const w = wrap360(facts.prime.w0 + facts.prime.rate * ((ms - J2000) / DAY));
|
|
65
|
+
const node = fromRaDec(ra + 90, 0);
|
|
66
|
+
const third = cross(pole, node);
|
|
67
|
+
const c = Math.cos(w * RAD);
|
|
68
|
+
const s = Math.sin(w * RAD);
|
|
69
|
+
meridian = [node[0] * c + third[0] * s, node[1] * c + third[1] * s, node[2] * c + third[2] * s];
|
|
70
|
+
}
|
|
71
|
+
return { pole: toScene(pole), meridian: toScene(meridian) };
|
|
72
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
import "../bodies/all.ts";
|
|
3
|
+
import { type Vec3 } from "../vec.ts";
|
|
4
|
+
import { moonOffset, orientation } from "./orient.ts";
|
|
5
|
+
export { moonOffset, orientation };
|
|
6
|
+
/**
|
|
7
|
+
* The looks the painter draws: `realistic` (shaded from the body's real colours and features) and `painted` (a public-domain surface map, with the shaded look until it arrives).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts no-run
|
|
11
|
+
* import { LOOKS, type PainterLook } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
12
|
+
*
|
|
13
|
+
* const first: PainterLook = LOOKS[0];
|
|
14
|
+
* console.log(first);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const LOOKS: readonly ["realistic", "painted"];
|
|
18
|
+
/**
|
|
19
|
+
* One of the looks in `LOOKS`: `realistic` or `painted`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts no-run
|
|
23
|
+
* import type { PainterLook } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
24
|
+
*
|
|
25
|
+
* const look: PainterLook = "painted";
|
|
26
|
+
* console.log(look);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export type PainterLook = (typeof LOOKS)[number];
|
|
30
|
+
/**
|
|
31
|
+
* What the caller gives the painter when it makes a body.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts no-run
|
|
35
|
+
* import type { PaintContext } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
36
|
+
*
|
|
37
|
+
* const context: PaintContext = { look: "painted", invalidate: () => undefined, ratio: 2 };
|
|
38
|
+
* console.log(context.look);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export interface PaintContext {
|
|
42
|
+
/** `realistic` (the default) or `painted`. */
|
|
43
|
+
look?: string;
|
|
44
|
+
/** Ask for a new draw: a sharper picture has arrived. */
|
|
45
|
+
invalidate?(): void;
|
|
46
|
+
/** Device pixels to a CSS pixel; default 1. */
|
|
47
|
+
ratio?: number;
|
|
48
|
+
/** The widest picture the GPU should be given, in pixels; default 2048. */
|
|
49
|
+
maxTexture?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What the caller tells a painted body at each draw: the direction to the Sun (a unit vector in the scene's axes), the time, and how many CSS pixels across its radius the body is on the screen.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts no-run
|
|
56
|
+
* import type { PaintInfo } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
57
|
+
*
|
|
58
|
+
* const info: PaintInfo = { toSun: [1, 0, 0], ms: Date.UTC(2026, 10, 2), radiusPx: 80 };
|
|
59
|
+
* console.log(info.radiusPx);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export interface PaintInfo {
|
|
63
|
+
toSun: Vec3;
|
|
64
|
+
ms: number;
|
|
65
|
+
radiusPx: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One body made by the painter: an object of radius 1 at the origin for the caller to move and scale.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts no-run
|
|
72
|
+
* import type { Painting } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
73
|
+
*
|
|
74
|
+
* const stop = (painting: Painting): void => painting.dispose();
|
|
75
|
+
* console.log(typeof stop);
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export interface Painting {
|
|
79
|
+
object: THREE.Object3D;
|
|
80
|
+
/** True to have the caller turn the object to face the camera (the Sun, which carries a glow). */
|
|
81
|
+
billboard?: boolean;
|
|
82
|
+
/** Call at each draw, after the object has been placed and scaled. */
|
|
83
|
+
update(info: PaintInfo): void;
|
|
84
|
+
dispose(): void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One moon of a planet at a time: where it is from the planet's centre and how big it is, both in radii of the planet, in the scene's axes.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts no-run
|
|
91
|
+
* import { painter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
92
|
+
*
|
|
93
|
+
* const io = painter.moons("Jupiter", Date.UTC(2026, 10, 2))[0];
|
|
94
|
+
* console.log(io?.name, io?.radius.toFixed(3));
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export interface MoonSite {
|
|
98
|
+
/** The moon's name as `make` takes it, for example `Io`. */
|
|
99
|
+
name: string;
|
|
100
|
+
offset: Vec3;
|
|
101
|
+
radius: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Where the pictures are, if not on jsDelivr: a folder address that ends in a slash. `farBase` holds the widest pictures (2048), which the npm package leaves out.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts no-run
|
|
108
|
+
* import { createPainter, type PainterOptions } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
109
|
+
*
|
|
110
|
+
* const options: PainterOptions = { textureBase: "/textures/", farBase: "/textures/" };
|
|
111
|
+
* console.log(createPainter(options).looks);
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export interface PainterOptions {
|
|
115
|
+
textureBase?: string;
|
|
116
|
+
farBase?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A painter: makes the bodies as three.js objects (`make`) and tells where a planet's moons are (`moons`). Its shape is chikyu-earth's `BodyPainter`, so `setBodyPainter(painter)` is all the wiring.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts no-run
|
|
123
|
+
* import { createPainter, type WakuseiPainter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
124
|
+
*
|
|
125
|
+
* const mine: WakuseiPainter = createPainter({ textureBase: "/pictures/", farBase: "/pictures/" });
|
|
126
|
+
* console.log(mine.looks.join(", "));
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export interface WakuseiPainter {
|
|
130
|
+
/** The looks `make` takes in `context.look`. */
|
|
131
|
+
readonly looks: readonly PainterLook[];
|
|
132
|
+
/** Make a body: `Sun`, a planet, `Pluto` or a moon, in any case. Throws for a name it does not know. */
|
|
133
|
+
/** `three` is the three.js module (or a subset of it holding the parts `PainterThree` names); it is typed loosely so that another package can hand over its own subset. */
|
|
134
|
+
make(three: object, name: string, context?: PaintContext): Painting;
|
|
135
|
+
/** The moons the painter draws for a planet (none for the Earth: its Moon is the viewer's own), where they are at a time, nearest first. */
|
|
136
|
+
moons(planet: string, ms: number): MoonSite[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Make a painter that takes its pictures from the given folders (jsDelivr's copy of this version by default).
|
|
140
|
+
*
|
|
141
|
+
* @param options - Where the pictures are.
|
|
142
|
+
* @returns The painter.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts no-run
|
|
146
|
+
* import { createPainter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
147
|
+
*
|
|
148
|
+
* const painter = createPainter();
|
|
149
|
+
* console.log(painter.moons("Mars", Date.UTC(2026, 10, 2)).map((m) => m.name));
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export declare function createPainter(options?: PainterOptions): WakuseiPainter;
|
|
153
|
+
/**
|
|
154
|
+
* The painter with its default pictures: pass it to chikyu-earth's `setBodyPainter`.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts no-run
|
|
158
|
+
* import { painter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
159
|
+
* import { setBodyPainter } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
160
|
+
*
|
|
161
|
+
* setBodyPainter(painter);
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export declare const painter: WakuseiPainter;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import "../bodies/all.js";
|
|
2
|
+
import { BODIES, MOONS_OF } from "../facts.js";
|
|
3
|
+
import { VERSION } from "../version.js";
|
|
4
|
+
import { cross } from "../vec.js";
|
|
5
|
+
import { bodyFragment, GLOW_FRAGMENT, GLOW_VERTEX, PAINTED_SURFACE, RING_FRAGMENT_WORLD, RING_VERTEX_WORLD, VERTEX_WORLD } from "./glsl.js";
|
|
6
|
+
import { lookOf } from "./look.js";
|
|
7
|
+
import { moonOffset, orientation } from "./orient.js";
|
|
8
|
+
import { ringStrip } from "./rings.js";
|
|
9
|
+
export { moonOffset, orientation };
|
|
10
|
+
/**
|
|
11
|
+
* The looks the painter draws: `realistic` (shaded from the body's real colours and features) and `painted` (a public-domain surface map, with the shaded look until it arrives).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts no-run
|
|
15
|
+
* import { LOOKS, type PainterLook } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
16
|
+
*
|
|
17
|
+
* const first: PainterLook = LOOKS[0];
|
|
18
|
+
* console.log(first);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const LOOKS = ["realistic", "painted"];
|
|
22
|
+
const TEXTURE_BASE = `https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets@${VERSION}/dist/textures/`;
|
|
23
|
+
const TEXTURE_FAR_BASE = `https://cdn.jsdelivr.net/gh/johnmorrisdotca/wakusei-planets@v${VERSION}/textures/`;
|
|
24
|
+
const slash = (base) => (base.endsWith("/") ? base : `${base}/`);
|
|
25
|
+
const titled = (id) => id.charAt(0).toUpperCase() + id.slice(1);
|
|
26
|
+
const pools = new WeakMap();
|
|
27
|
+
function acquire(three, key, make) {
|
|
28
|
+
let pool = pools.get(three);
|
|
29
|
+
if (pool === undefined)
|
|
30
|
+
pools.set(three, (pool = new Map()));
|
|
31
|
+
let held = pool.get(key);
|
|
32
|
+
if (held === undefined)
|
|
33
|
+
pool.set(key, (held = { value: make(), users: 0 }));
|
|
34
|
+
held.users += 1;
|
|
35
|
+
const mine = held;
|
|
36
|
+
const owner = pool;
|
|
37
|
+
let released = false;
|
|
38
|
+
return {
|
|
39
|
+
value: mine.value,
|
|
40
|
+
release() {
|
|
41
|
+
if (released)
|
|
42
|
+
return;
|
|
43
|
+
released = true;
|
|
44
|
+
mine.users -= 1;
|
|
45
|
+
if (mine.users <= 0) {
|
|
46
|
+
mine.value.dispose();
|
|
47
|
+
owner.delete(key);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const fallbackLook = (id) => {
|
|
53
|
+
const c = [1, 3, 5].map((i) => (parseInt(BODIES[id].colour.slice(i, i + 2), 16) / 255).toFixed(3)).join(", ");
|
|
54
|
+
return { id, surface: `vec4 surface(vec3 p) { float n = fbm(p * 5.0); return vec4(vec3(${c}) * (0.85 + 0.3 * n), 0.0); }` };
|
|
55
|
+
};
|
|
56
|
+
/** The name of a body as the painter takes it: any case, `Sun`, a planet or Pluto, or a moon such as `Io`. */
|
|
57
|
+
function idOf(name) {
|
|
58
|
+
const id = name.toLowerCase();
|
|
59
|
+
if (!(id in BODIES))
|
|
60
|
+
throw new Error(`wakusei-planets: no body called ${name}`);
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
63
|
+
const moonsOf = (planet) => {
|
|
64
|
+
const id = planet.toLowerCase();
|
|
65
|
+
return id === "earth" ? [] : id in MOONS_OF ? MOONS_OF[id] : [];
|
|
66
|
+
};
|
|
67
|
+
function paint(T, id, context, bases) {
|
|
68
|
+
const facts = BODIES[id];
|
|
69
|
+
const look = lookOf(id) ?? fallbackLook(id);
|
|
70
|
+
const wantPainted = (context.look === "painted" || look.mapped === true) && look.painted !== undefined;
|
|
71
|
+
const ratio = context.ratio ?? 1;
|
|
72
|
+
const maxWidth = context.maxTexture ?? 2048;
|
|
73
|
+
const redraw = () => context.invalidate?.();
|
|
74
|
+
const releases = [];
|
|
75
|
+
const disposables = [];
|
|
76
|
+
const sphere = acquire(T, "sphere", () => new T.SphereGeometry(1, 96, 48));
|
|
77
|
+
releases.push(sphere.release);
|
|
78
|
+
const group = new T.Group();
|
|
79
|
+
group.name = facts.name.en;
|
|
80
|
+
const uniforms = (painted) => {
|
|
81
|
+
const a = look.atmosphere;
|
|
82
|
+
return {
|
|
83
|
+
uLight: { value: new T.Vector3(1, 0, 0) },
|
|
84
|
+
uRelief: { value: painted ? (look.painted?.relief ?? 0) : (look.relief ?? 0.6) },
|
|
85
|
+
uAmbient: { value: look.ambient ?? 0.04 },
|
|
86
|
+
uLimb: { value: look.limb ?? 0 },
|
|
87
|
+
uWrap: { value: look.wrap ?? (a === undefined ? 0.04 : 0.3) },
|
|
88
|
+
uAtmos: { value: new T.Vector3(...(a?.colour ?? [0, 0, 0])) },
|
|
89
|
+
uAtmosPow: { value: a?.power ?? 3 },
|
|
90
|
+
uEmissive: { value: look.emissive === true ? 1 : 0 },
|
|
91
|
+
uRingN: { value: new T.Vector3(0, 1, 0) },
|
|
92
|
+
uRingR: { value: new T.Vector2(1, 2) },
|
|
93
|
+
uRingOn: { value: 0 },
|
|
94
|
+
uRingTex: { value: null },
|
|
95
|
+
uMap: { value: null },
|
|
96
|
+
uScale: { value: 1 },
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const material = (painted) => {
|
|
100
|
+
const m = new T.ShaderMaterial({ vertexShader: VERTEX_WORLD, fragmentShader: bodyFragment(painted ? PAINTED_SURFACE : look.surface, painted, true), uniforms: uniforms(painted) });
|
|
101
|
+
disposables.push(m);
|
|
102
|
+
return m;
|
|
103
|
+
};
|
|
104
|
+
const shader = material(false);
|
|
105
|
+
const paintedMaterial = wantPainted ? material(true) : undefined;
|
|
106
|
+
const materials = [shader, ...(paintedMaterial ? [paintedMaterial] : [])];
|
|
107
|
+
const mesh = new T.Mesh(sphere.value, shader);
|
|
108
|
+
mesh.matrixAutoUpdate = false;
|
|
109
|
+
group.add(mesh);
|
|
110
|
+
// rings: the table's strip at once, the picture of them (painted) when it arrives
|
|
111
|
+
let ringMesh;
|
|
112
|
+
let ringMaterial;
|
|
113
|
+
let ringTexture;
|
|
114
|
+
let ringInner = 1;
|
|
115
|
+
let ringOuter = 2;
|
|
116
|
+
const table = look.emissive === true ? null : ringStrip(facts, 1024);
|
|
117
|
+
const loadImage = (url) => new Promise((resolve, reject) => {
|
|
118
|
+
const loader = new T.TextureLoader();
|
|
119
|
+
loader.setCrossOrigin("anonymous");
|
|
120
|
+
loader.load(url, resolve, undefined, (e) => reject(e instanceof Error ? e : new Error("texture failed")));
|
|
121
|
+
});
|
|
122
|
+
let gone = false;
|
|
123
|
+
if (table !== null) {
|
|
124
|
+
ringInner = table.inner;
|
|
125
|
+
ringOuter = table.outer;
|
|
126
|
+
const strip = new T.DataTexture(table.data, table.width, 1, T.RGBAFormat);
|
|
127
|
+
strip.magFilter = T.LinearFilter;
|
|
128
|
+
strip.minFilter = T.LinearFilter;
|
|
129
|
+
strip.wrapS = T.ClampToEdgeWrapping;
|
|
130
|
+
strip.needsUpdate = true;
|
|
131
|
+
ringTexture = strip;
|
|
132
|
+
const ringGeometry = new T.RingGeometry(ringInner, ringOuter, 256, 1);
|
|
133
|
+
disposables.push(ringGeometry);
|
|
134
|
+
ringMaterial = new T.ShaderMaterial({
|
|
135
|
+
vertexShader: RING_VERTEX_WORLD,
|
|
136
|
+
fragmentShader: RING_FRAGMENT_WORLD,
|
|
137
|
+
side: T.DoubleSide,
|
|
138
|
+
transparent: true,
|
|
139
|
+
depthWrite: false,
|
|
140
|
+
uniforms: { uRingTex: { value: strip }, uRingR: { value: new T.Vector2(ringInner, ringOuter) }, uLight: { value: new T.Vector3(1, 0, 0) }, uRingN: { value: new T.Vector3(0, 1, 0) }, uScale: { value: 1 } },
|
|
141
|
+
});
|
|
142
|
+
disposables.push(ringMaterial);
|
|
143
|
+
ringMesh = new T.Mesh(ringGeometry, ringMaterial);
|
|
144
|
+
ringMesh.matrixAutoUpdate = false;
|
|
145
|
+
ringMesh.renderOrder = 1;
|
|
146
|
+
group.add(ringMesh);
|
|
147
|
+
const photo = wantPainted ? look.painted?.rings : undefined;
|
|
148
|
+
if (photo !== undefined) {
|
|
149
|
+
void loadImage(`${bases.near}${photo.map}-${photo.width}.webp`).then((t) => {
|
|
150
|
+
if (gone) {
|
|
151
|
+
t.dispose();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
t.magFilter = T.LinearFilter;
|
|
155
|
+
t.minFilter = T.LinearFilter;
|
|
156
|
+
t.generateMipmaps = false;
|
|
157
|
+
t.wrapS = T.ClampToEdgeWrapping;
|
|
158
|
+
t.colorSpace = T.NoColorSpace;
|
|
159
|
+
ringTexture?.dispose();
|
|
160
|
+
ringTexture = t;
|
|
161
|
+
ringInner = photo.from / facts.radiusKm;
|
|
162
|
+
ringOuter = photo.to / facts.radiusKm;
|
|
163
|
+
ringMesh.geometry.dispose();
|
|
164
|
+
ringMesh.geometry = new T.RingGeometry(ringInner, ringOuter, 256, 1);
|
|
165
|
+
disposables.push(ringMesh.geometry);
|
|
166
|
+
ringMaterial.uniforms.uRingTex.value = t;
|
|
167
|
+
ringMaterial.uniforms.uRingR.value.set(ringInner, ringOuter);
|
|
168
|
+
for (const m of materials) {
|
|
169
|
+
m.uniforms.uRingTex.value = t;
|
|
170
|
+
m.uniforms.uRingR.value.set(ringInner, ringOuter);
|
|
171
|
+
}
|
|
172
|
+
redraw();
|
|
173
|
+
}, () => undefined);
|
|
174
|
+
}
|
|
175
|
+
for (const m of materials) {
|
|
176
|
+
m.uniforms.uRingOn.value = 1;
|
|
177
|
+
m.uniforms.uRingTex.value = strip;
|
|
178
|
+
m.uniforms.uRingR.value.set(ringInner, ringOuter);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// the glow of a star: a plane through its centre that the caller turns to the camera
|
|
182
|
+
if (look.emissive === true) {
|
|
183
|
+
const glowGeometry = acquire(T, "glow", () => new T.PlaneGeometry(6, 6));
|
|
184
|
+
releases.push(glowGeometry.release);
|
|
185
|
+
const glowMaterial = new T.ShaderMaterial({ vertexShader: GLOW_VERTEX, fragmentShader: GLOW_FRAGMENT, transparent: true, depthWrite: false, blending: T.AdditiveBlending, uniforms: { uColour: { value: new T.Vector3(1, 0.7, 0.3) } } });
|
|
186
|
+
disposables.push(glowMaterial);
|
|
187
|
+
const glow = new T.Mesh(glowGeometry.value, glowMaterial);
|
|
188
|
+
glow.renderOrder = 4;
|
|
189
|
+
glow.frustumCulled = false;
|
|
190
|
+
group.add(glow);
|
|
191
|
+
}
|
|
192
|
+
// the painted map: the smallest width at once, a sharper one when the body is big on the screen, back to the small one when it is not
|
|
193
|
+
const widths = (look.painted?.widths ?? []).filter((w, i) => i === 0 || w <= maxWidth);
|
|
194
|
+
let small;
|
|
195
|
+
let sharp;
|
|
196
|
+
let sharpWidth = 0;
|
|
197
|
+
let loading = false;
|
|
198
|
+
let failed = false;
|
|
199
|
+
let tooBigSince = 0;
|
|
200
|
+
const tried = new Set();
|
|
201
|
+
const urlOf = (w) => `${look.painted.far !== undefined && w >= look.painted.far ? bases.far : bases.near}${look.painted.map}-${w}.webp`;
|
|
202
|
+
const prepare = (t) => {
|
|
203
|
+
t.anisotropy = 4;
|
|
204
|
+
t.wrapS = T.RepeatWrapping;
|
|
205
|
+
t.colorSpace = T.NoColorSpace;
|
|
206
|
+
return t;
|
|
207
|
+
};
|
|
208
|
+
const show = (t) => {
|
|
209
|
+
paintedMaterial.uniforms.uMap.value = t;
|
|
210
|
+
mesh.material = paintedMaterial;
|
|
211
|
+
redraw();
|
|
212
|
+
};
|
|
213
|
+
if (wantPainted && paintedMaterial !== undefined && widths[0] !== undefined) {
|
|
214
|
+
void loadImage(urlOf(widths[0])).then((t) => {
|
|
215
|
+
if (gone) {
|
|
216
|
+
t.dispose();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
small = prepare(t);
|
|
220
|
+
if (sharp === undefined)
|
|
221
|
+
show(small);
|
|
222
|
+
}, () => {
|
|
223
|
+
failed = true;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const keepSized = (radiusPx) => {
|
|
227
|
+
if (!wantPainted || failed || small === undefined || loading)
|
|
228
|
+
return;
|
|
229
|
+
const wanted = radiusPx * 2 * ratio * 1.6;
|
|
230
|
+
const next = widths.find((w) => w >= wanted) ?? widths[widths.length - 1];
|
|
231
|
+
if (next > sharpWidth && next > widths[0] && !tried.has(next)) {
|
|
232
|
+
tried.add(next);
|
|
233
|
+
loading = true;
|
|
234
|
+
void loadImage(urlOf(next)).then((t) => {
|
|
235
|
+
loading = false;
|
|
236
|
+
if (gone) {
|
|
237
|
+
t.dispose();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const old = sharp;
|
|
241
|
+
sharp = prepare(t);
|
|
242
|
+
sharpWidth = next;
|
|
243
|
+
show(sharp);
|
|
244
|
+
old?.dispose();
|
|
245
|
+
}, () => {
|
|
246
|
+
loading = false;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
else if (sharp !== undefined && wanted * 2 < sharpWidth) {
|
|
250
|
+
// much smaller on the screen than the picture it holds: after a few seconds, let the big picture go
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
if (tooBigSince === 0)
|
|
253
|
+
tooBigSince = now;
|
|
254
|
+
else if (now - tooBigSince > 4000) {
|
|
255
|
+
show(small);
|
|
256
|
+
sharp.dispose();
|
|
257
|
+
sharp = undefined;
|
|
258
|
+
sharpWidth = 0;
|
|
259
|
+
tried.clear();
|
|
260
|
+
tooBigSince = 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else
|
|
264
|
+
tooBigSince = 0;
|
|
265
|
+
};
|
|
266
|
+
const axes = facts.axesKm;
|
|
267
|
+
const scaleX = axes ? axes[1] / facts.radiusKm : 1;
|
|
268
|
+
const scaleY = axes ? axes[2] / facts.radiusKm : 1 - facts.flattening;
|
|
269
|
+
const scaleZ = axes ? axes[0] / facts.radiusKm : 1;
|
|
270
|
+
const basis = (object, x, y, z, sx = 1, sy = 1, sz = 1) => {
|
|
271
|
+
object.matrix.makeBasis(new T.Vector3(x[0] * sx, x[1] * sx, x[2] * sx), new T.Vector3(y[0] * sy, y[1] * sy, y[2] * sy), new T.Vector3(z[0] * sz, z[1] * sz, z[2] * sz));
|
|
272
|
+
};
|
|
273
|
+
const light = new T.Vector3();
|
|
274
|
+
const poleVec = new T.Vector3();
|
|
275
|
+
return {
|
|
276
|
+
object: group,
|
|
277
|
+
...(look.emissive === true ? { billboard: true } : {}),
|
|
278
|
+
update(info) {
|
|
279
|
+
light.set(info.toSun[0], info.toSun[1], info.toSun[2]);
|
|
280
|
+
const scale = group.scale.x || 1;
|
|
281
|
+
if (look.emissive !== true) {
|
|
282
|
+
const { pole, meridian } = orientation(id, info.ms);
|
|
283
|
+
const east = cross(pole, meridian);
|
|
284
|
+
basis(mesh, east, pole, meridian, scaleX, scaleY, scaleZ);
|
|
285
|
+
if (ringMesh !== undefined)
|
|
286
|
+
basis(ringMesh, meridian, east, pole);
|
|
287
|
+
poleVec.set(pole[0], pole[1], pole[2]);
|
|
288
|
+
}
|
|
289
|
+
for (const m of materials) {
|
|
290
|
+
m.uniforms.uLight.value.copy(light);
|
|
291
|
+
m.uniforms.uScale.value = scale;
|
|
292
|
+
m.uniforms.uRingN.value.copy(poleVec);
|
|
293
|
+
}
|
|
294
|
+
if (ringMaterial !== undefined) {
|
|
295
|
+
ringMaterial.uniforms.uLight.value.copy(light);
|
|
296
|
+
ringMaterial.uniforms.uRingN.value.copy(poleVec);
|
|
297
|
+
ringMaterial.uniforms.uScale.value = scale;
|
|
298
|
+
}
|
|
299
|
+
keepSized(info.radiusPx);
|
|
300
|
+
},
|
|
301
|
+
dispose() {
|
|
302
|
+
gone = true;
|
|
303
|
+
small?.dispose();
|
|
304
|
+
sharp?.dispose();
|
|
305
|
+
ringTexture?.dispose();
|
|
306
|
+
for (const d of disposables)
|
|
307
|
+
d.dispose();
|
|
308
|
+
for (const r of releases)
|
|
309
|
+
r();
|
|
310
|
+
group.clear();
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Make a painter that takes its pictures from the given folders (jsDelivr's copy of this version by default).
|
|
316
|
+
*
|
|
317
|
+
* @param options - Where the pictures are.
|
|
318
|
+
* @returns The painter.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts no-run
|
|
322
|
+
* import { createPainter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
323
|
+
*
|
|
324
|
+
* const painter = createPainter();
|
|
325
|
+
* console.log(painter.moons("Mars", Date.UTC(2026, 10, 2)).map((m) => m.name));
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
export function createPainter(options = {}) {
|
|
329
|
+
const near = slash(options.textureBase ?? TEXTURE_BASE);
|
|
330
|
+
const far = slash(options.farBase ?? (options.textureBase !== undefined ? options.textureBase : TEXTURE_FAR_BASE));
|
|
331
|
+
return {
|
|
332
|
+
looks: LOOKS,
|
|
333
|
+
make: (three, name, context = {}) => paint(three, idOf(name), context, { near, far }),
|
|
334
|
+
moons(planet, ms) {
|
|
335
|
+
const radius = BODIES[idOf(planet)]?.radiusKm;
|
|
336
|
+
if (radius === undefined)
|
|
337
|
+
return [];
|
|
338
|
+
return moonsOf(planet)
|
|
339
|
+
.map((moon) => ({ name: titled(moon), offset: moonOffset(moon, ms), radius: BODIES[moon].radiusKm / radius }))
|
|
340
|
+
.sort((a, b) => Math.hypot(...a.offset) - Math.hypot(...b.offset));
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The painter with its default pictures: pass it to chikyu-earth's `setBodyPainter`.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts no-run
|
|
349
|
+
* import { painter } from "@johnmorrisdotca/wakusei-planets/painter";
|
|
350
|
+
* import { setBodyPainter } from "@johnmorrisdotca/chikyu-earth/space/element";
|
|
351
|
+
*
|
|
352
|
+
* setBodyPainter(painter);
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
export const painter = createPainter();
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.5.1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.5.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/wakusei-planets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Wakusei Planets 惑星 — The Sun, planets and moons for the web: each as a clean line drawing, a shaded realistic drawing or a painted surface map, with real sizes, axial tilt and rotation as data, and positions of the moons for any date and time. SVG for the lines, three.js (optional) for the rest.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"planets",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"import": "./dist/scene/look.js",
|
|
58
58
|
"default": "./dist/scene/look.js"
|
|
59
59
|
},
|
|
60
|
+
"./painter": {
|
|
61
|
+
"types": "./dist/scene/painter.d.ts",
|
|
62
|
+
"import": "./dist/scene/painter.js",
|
|
63
|
+
"default": "./dist/scene/painter.js"
|
|
64
|
+
},
|
|
60
65
|
"./all": {
|
|
61
66
|
"types": "./dist/bodies/all.d.ts",
|
|
62
67
|
"import": "./dist/bodies/all.js",
|
|
@@ -179,7 +184,8 @@
|
|
|
179
184
|
"docs:check": "pnpm build && node scripts/docs-site.mjs check",
|
|
180
185
|
"size": "node scripts/docs-site.mjs size",
|
|
181
186
|
"test:usage": "pnpm site && node scripts/check-usage.ts",
|
|
182
|
-
"check:standard": "node scripts/check-standard.mjs"
|
|
187
|
+
"check:standard": "node scripts/check-standard.mjs",
|
|
188
|
+
"test:readme": "pnpm build && node scripts/check-usage.ts readme"
|
|
183
189
|
},
|
|
184
190
|
"devDependencies": {
|
|
185
191
|
"@angular/common": "^22.2.4",
|
|
@@ -217,7 +223,7 @@
|
|
|
217
223
|
},
|
|
218
224
|
"packageManager": "pnpm@10.28.0",
|
|
219
225
|
"dependencies": {
|
|
220
|
-
"@johnmorrisdotca/kazoku-ui": "^0.
|
|
226
|
+
"@johnmorrisdotca/kazoku-ui": "^0.12.0"
|
|
221
227
|
},
|
|
222
228
|
"peerDependencies": {
|
|
223
229
|
"@johnmorrisdotca/chikyu-earth": ">=0.10.0",
|