@johnmorrisdotca/wakusei-planets 0.7.0 → 0.11.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/NOTICE.md +16 -1
  3. package/README.md +6 -6
  4. package/dist/bodies/all.d.ts +2 -0
  5. package/dist/bodies/all.js +2 -0
  6. package/dist/bodies/dwarfs.d.ts +1 -0
  7. package/dist/bodies/dwarfs.js +32 -0
  8. package/dist/bodies/moons-dwarf.d.ts +1 -0
  9. package/dist/bodies/moons-dwarf.js +6 -0
  10. package/dist/bodies/moons-neptune.js +1 -0
  11. package/dist/bodies/moons-pluto.js +1 -1
  12. package/dist/bodies/moons-saturn.js +3 -2
  13. package/dist/data/dwarf.d.ts +16 -0
  14. package/dist/data/dwarf.js +24 -0
  15. package/dist/data/orbits.d.ts +2 -2
  16. package/dist/data/orbits.js +4 -0
  17. package/dist/element.d.ts +4 -2
  18. package/dist/element.js +4 -0
  19. package/dist/embed.d.ts +1 -1
  20. package/dist/embed.js +5 -3
  21. package/dist/ephemeris.d.ts +6 -6
  22. package/dist/ephemeris.js +33 -12
  23. package/dist/facts.d.ts +44 -6
  24. package/dist/facts.js +32 -3
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/line.js +7 -4
  28. package/dist/portrait/model.d.ts +2 -2
  29. package/dist/portrait/model.js +138 -5
  30. package/dist/portrait/overlay.js +2 -1
  31. package/dist/pose.d.ts +7 -5
  32. package/dist/pose.js +25 -11
  33. package/dist/scene/glsl.d.ts +2 -2
  34. package/dist/scene/glsl.js +10 -6
  35. package/dist/textures/ceres-1024.webp +0 -0
  36. package/dist/textures/ceres-512.webp +0 -0
  37. package/dist/textures/charon-1024.webp +0 -0
  38. package/dist/textures/charon-512.webp +0 -0
  39. package/dist/textures/dione-1024.webp +0 -0
  40. package/dist/textures/dione-512.webp +0 -0
  41. package/dist/textures/iapetus-1024.webp +0 -0
  42. package/dist/textures/iapetus-512.webp +0 -0
  43. package/dist/textures/tethys-1024.webp +0 -0
  44. package/dist/textures/tethys-512.webp +0 -0
  45. package/dist/textures/triton-1024.webp +0 -0
  46. package/dist/textures/triton-512.webp +0 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +11 -1
package/dist/facts.d.ts CHANGED
@@ -10,7 +10,18 @@
10
10
  */
11
11
  export declare const PLANET_IDS: readonly ["mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"];
12
12
  /**
13
- * The moons the package draws, by planet.
13
+ * The dwarf planets besides Pluto, nearest the Sun first (by the size of their orbits).
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { DWARF_IDS } from "@johnmorrisdotca/wakusei-planets";
18
+ *
19
+ * console.log(DWARF_IDS.join(", "));
20
+ * ```
21
+ */
22
+ export declare const DWARF_IDS: readonly ["ceres", "orcus", "haumea", "quaoar", "makemake", "gonggong", "eris", "sedna"];
23
+ /**
24
+ * The moons the package draws, by planet or dwarf planet.
14
25
  *
15
26
  * @example
16
27
  * ```ts
@@ -27,6 +38,9 @@ export declare const MOONS_OF: {
27
38
  readonly uranus: readonly ["miranda", "ariel", "umbriel", "titania", "oberon"];
28
39
  readonly neptune: readonly ["proteus", "triton", "nereid"];
29
40
  readonly pluto: readonly ["charon", "nix", "hydra"];
41
+ readonly orcus: readonly ["vanth"];
42
+ readonly haumea: readonly ["hiiaka", "namaka"];
43
+ readonly eris: readonly ["dysnomia"];
30
44
  };
31
45
  /**
32
46
  * The name of a planet or of Pluto: `mercury` to `pluto`.
@@ -40,6 +54,30 @@ export declare const MOONS_OF: {
40
54
  * ```
41
55
  */
42
56
  export type PlanetId = (typeof PLANET_IDS)[number];
57
+ /**
58
+ * The name of a dwarf planet besides Pluto: `ceres`, `eris`, `haumea`...
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { DWARF_IDS, type DwarfId } from "@johnmorrisdotca/wakusei-planets";
63
+ *
64
+ * const id: DwarfId = DWARF_IDS[0];
65
+ * console.log(id);
66
+ * ```
67
+ */
68
+ export type DwarfId = (typeof DWARF_IDS)[number];
69
+ /**
70
+ * A body a moon can go round: a planet, Pluto or another dwarf planet.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * import { MOONS_OF, type HostId } from "@johnmorrisdotca/wakusei-planets";
75
+ *
76
+ * const host: HostId = "eris";
77
+ * console.log(MOONS_OF[host]);
78
+ * ```
79
+ */
80
+ export type HostId = PlanetId | DwarfId;
43
81
  /**
44
82
  * The name of a moon the package draws, such as `moon`, `io` or `titan`.
45
83
  *
@@ -53,7 +91,7 @@ export type PlanetId = (typeof PLANET_IDS)[number];
53
91
  */
54
92
  export type MoonId = (typeof MOONS_OF)[keyof typeof MOONS_OF][number];
55
93
  /**
56
- * The name of a body: `sun`, a planet or a moon.
94
+ * The name of a body: `sun`, a planet, a dwarf planet or a moon.
57
95
  *
58
96
  * @example
59
97
  * ```ts
@@ -63,7 +101,7 @@ export type MoonId = (typeof MOONS_OF)[keyof typeof MOONS_OF][number];
63
101
  * console.log(lineSvg(id, { size: 64 }).length > 100);
64
102
  * ```
65
103
  */
66
- export type BodyId = "sun" | PlanetId | MoonId;
104
+ export type BodyId = "sun" | PlanetId | DwarfId | MoonId;
67
105
  /**
68
106
  * How a body is drawn: `line` (an outline drawing), `realistic` (shaded, from the numbers) or `painted` (a real surface map).
69
107
  *
@@ -139,8 +177,8 @@ export type Feature = {
139
177
  export interface BodyFacts {
140
178
  id: BodyId;
141
179
  kind: "star" | "planet" | "dwarf-planet" | "moon";
142
- /** The planet a moon goes round. */
143
- parent?: PlanetId;
180
+ /** The planet or dwarf planet a moon goes round. */
181
+ parent?: HostId;
144
182
  name: {
145
183
  en: string;
146
184
  ja: string;
@@ -186,7 +224,7 @@ export interface BodyFacts {
186
224
  */
187
225
  export declare const BODIES: Readonly<Record<BodyId, BodyFacts>>;
188
226
  /**
189
- * Every body's name: the Sun, the planets outward, then the moons of each planet.
227
+ * Every body's name: the Sun, the planets outward, the dwarf planets, then the moons of each.
190
228
  *
191
229
  * @example
192
230
  * ```ts
package/dist/facts.js CHANGED
@@ -12,7 +12,18 @@
12
12
  */
13
13
  export const PLANET_IDS = ["mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto"];
14
14
  /**
15
- * The moons the package draws, by planet.
15
+ * The dwarf planets besides Pluto, nearest the Sun first (by the size of their orbits).
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { DWARF_IDS } from "@johnmorrisdotca/wakusei-planets";
20
+ *
21
+ * console.log(DWARF_IDS.join(", "));
22
+ * ```
23
+ */
24
+ export const DWARF_IDS = ["ceres", "orcus", "haumea", "quaoar", "makemake", "gonggong", "eris", "sedna"];
25
+ /**
26
+ * The moons the package draws, by planet or dwarf planet.
16
27
  *
17
28
  * @example
18
29
  * ```ts
@@ -29,6 +40,9 @@ export const MOONS_OF = {
29
40
  uranus: ["miranda", "ariel", "umbriel", "titania", "oberon"],
30
41
  neptune: ["proteus", "triton", "nereid"],
31
42
  pluto: ["charon", "nix", "hydra"],
43
+ orcus: ["vanth"],
44
+ haumea: ["hiiaka", "namaka"],
45
+ eris: ["dysnomia"],
32
46
  };
33
47
  const f = (id, kind, en, ja, radiusKm, flattening, tiltDeg, ra, dec, w0, rate, rotationHours, colour, more = {}) => ({
34
48
  id,
@@ -131,6 +145,17 @@ export const BODIES = {
131
145
  ...dots([[-60, 20, 6], [-100, -20, 7], [60, 40, 5]]),
132
146
  ],
133
147
  }),
148
+ ceres: f("ceres", "dwarf-planet", "Ceres", "ケレス", 481.05, 0.0731, 4.04, 291.42744, 66.76033, 170.65, 952.1532, 9.07417, "#7b7873", {
149
+ showLon: 239,
150
+ features: [{ kind: "circle", lon: 239, lat: 20, radius: 6 }, { kind: "circle", lon: 239, lat: 20, radius: 1.5, fill: true }, { kind: "circle", lon: 10, lat: -42, radius: 10 }, ...dots([[-60, 30, 8], [100, 15, 9], [160, -20, 7], [-120, -35, 8], [40, 45, 6], [-20, 0, 5]])],
151
+ }),
152
+ orcus: f("orcus", "dwarf-planet", "Orcus", "オルクス", 455, 0, 73.83, 142.474, 15.004, 0, 655.15, 13.188, "#9a9a9c", { features: dots([[-60, 20, 9], [40, -15, 7], [130, 30, 8], [-130, -30, 6]]) }),
153
+ haumea: f("haumea", "dwarf-planet", "Haumea", "ハウメア", 798, 0, 90.15, 282.6, -13, 0, 2206.6, 3.915341, "#dfe0e3", { showLon: 90, axesKm: [1161, 852, 513], features: [{ kind: "oval", lon: 40, lat: 5, rx: 22, ry: 12, fill: true }, { kind: "band", lat: 0 }, ...dots([[-80, 25, 7]])] }),
154
+ quaoar: f("quaoar", "dwarf-planet", "Quaoar", "クワオアー", 557.8, 0, 0, 265.4, 74.409, 0, 488.72, 17.6788, "#a3735a", { showLon: 90, axesKm: [620.7, 546.5, 511.5], features: dots([[-40, 20, 8], [70, -15, 9], [150, 35, 7], [-110, -30, 6]]) }),
155
+ makemake: f("makemake", "dwarf-planet", "Makemake", "マケマケ", 715, 0.01, 0, 317.92, 50.03, 0, 378.5, 22.8266, "#b87f5d", { features: [{ kind: "oval", lon: 30, lat: 10, rx: 30, ry: 18, fill: true }, ...dots([[-70, -20, 8], [120, 25, 7]])] }),
156
+ gonggong: f("gonggong", "dwarf-planet", "Gonggong", "ゴンゴン", 615, 0, 0, 255.383, 36.831, 0, 385.7, 22.4, "#9c4f3b", { features: dots([[-50, 10, 9], [60, -25, 8], [140, 30, 7]]) }),
157
+ eris: f("eris", "dwarf-planet", "Eris", "エリス", 1163, 0, 78.22, 36.17, 44.51, 0, 22.805163, 378.8616, "#e8e4dc", { features: [{ kind: "band", lat: 0 }, ...dots([[-30, 30, 8], [80, -20, 9]])] }),
158
+ sedna: f("sedna", "dwarf-planet", "Sedna", "セドナ", 453, 0, 0, 297.062, 74.71, 0, 840.95, 10.273, "#9a3f2b", { features: dots([[-40, 20, 9], [70, -30, 8], [150, 10, 7]]) }),
134
159
  moon: f("moon", "moon", "Moon", "月", 1737.4, 0.0012, 6.68, 269.9949, 66.5392, 38.3213, 13.17635815, 655.728, "#b8b5ae", {
135
160
  parent: "earth",
136
161
  synchronous: true,
@@ -169,9 +194,13 @@ export const BODIES = {
169
194
  charon: f("charon", "moon", "Charon", "カロン", 606, 0, 0, 132.993, -6.163, 122.695, 56.3625225, 153.2935, "#9c9189", { parent: "pluto", synchronous: true, features: [{ kind: "oval", lon: 0, lat: 76, rx: 90, ry: 14, fill: true }, { kind: "circle", lon: 40, lat: -15, radius: 12 }, ...dots([[-60, 10, 8], [120, 30, 7]])] }),
170
195
  nix: f("nix", "moon", "Nix", "ニクス", 19.3, 0.3, 0, 132.993, -6.163, 0, 14.48, 0, "#b3ada7", { parent: "pluto", axesKm: [24.9, 16.6, 15.6], features: [{ kind: "circle", lon: 0, lat: 0, radius: 18 }] }),
171
196
  hydra: f("hydra", "moon", "Hydra", "ヒドラ", 19.5, 0.4, 0, 132.993, -6.163, 0, 9.42, 0, "#c4beb6", { parent: "pluto", axesKm: [32.5, 22.5, 12.5], features: [{ kind: "circle", lon: 30, lat: 10, radius: 20 }] }),
197
+ vanth: f("vanth", "moon", "Vanth", "ヴァント", 221, 0, 0, 142.474, 15.004, 0, 37.738618, 228.9488, "#6e6a66", { parent: "orcus", synchronous: true, features: dots([[-50, 15, 10], [80, -20, 9], [150, 30, 8]]) }),
198
+ hiiaka: f("hiiaka", "moon", "Hiʻiaka", "ヒイアカ", 185, 0, 0, 282.6, -13, 0, 0, 0, "#e0e0e0", { parent: "haumea", features: dots([[-30, 20, 12], [70, -25, 10]]) }),
199
+ namaka: f("namaka", "moon", "Namaka", "ナマカ", 75, 0, 0, 282.6, -13, 0, 0, 0, "#d0d0d0", { parent: "haumea", features: dots([[0, 0, 14]]) }),
200
+ dysnomia: f("dysnomia", "moon", "Dysnomia", "ディスノミア", 308, 0, 0, 36.17, 44.51, 0, 22.805163, 378.8616, "#5a5856", { parent: "eris", synchronous: true, features: dots([[-60, 20, 10], [50, -25, 9], [140, 30, 8]]) }),
172
201
  };
173
202
  /**
174
- * Every body's name: the Sun, the planets outward, then the moons of each planet.
203
+ * Every body's name: the Sun, the planets outward, the dwarf planets, then the moons of each.
175
204
  *
176
205
  * @example
177
206
  * ```ts
@@ -180,7 +209,7 @@ export const BODIES = {
180
209
  * console.log(BODY_IDS.length, BODY_IDS[0]);
181
210
  * ```
182
211
  */
183
- export const BODY_IDS = ["sun", ...PLANET_IDS, ...Object.values(MOONS_OF).flat()];
212
+ export const BODY_IDS = ["sun", ...PLANET_IDS, ...DWARF_IDS, ...Object.values(MOONS_OF).flat()];
184
213
  /** A body's polar radius as a fraction of its equatorial radius. */
185
214
  export const polarRatio = (facts) => 1 - facts.flattening;
186
215
  /**
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { BODIES, BODY_IDS, MOONS_OF, PLANET_IDS, ringReach, type BodyFacts, type BodyId, type Feature, type Look, type MoonId, type PlanetId, type RingBand } from "./facts.ts";
1
+ export { BODIES, BODY_IDS, DWARF_IDS, MOONS_OF, PLANET_IDS, ringReach, type BodyFacts, type BodyId, type DwarfId, type Feature, type HostId, type Look, type MoonId, type PlanetId, type RingBand } from "./facts.ts";
2
2
  export { apparent, moonGeocentric, heliocentric, orbitPath, system, type Apparent, type MoonPlace, type Pose, type System } from "./ephemeris.ts";
3
3
  export { drawnTilt, portrait, type PortraitOptions } from "./pose.ts";
4
4
  export { lineSvg, mix, type LineOptions } from "./line.ts";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { BODIES, BODY_IDS, MOONS_OF, PLANET_IDS, ringReach } from "./facts.js";
1
+ export { BODIES, BODY_IDS, DWARF_IDS, MOONS_OF, PLANET_IDS, ringReach } from "./facts.js";
2
2
  export { apparent, moonGeocentric, heliocentric, orbitPath, system } from "./ephemeris.js";
3
3
  export { drawnTilt, portrait } from "./pose.js";
4
4
  export { lineSvg, mix } from "./line.js";
package/dist/line.js CHANGED
@@ -54,12 +54,15 @@ function around(lon, lat, rx, ry, steps = 28) {
54
54
  }
55
55
  return out;
56
56
  }
57
+ /** The semi-axes of a body that is far from round, as fractions of its radius. */
58
+ const axesOf = (facts) => (facts.axesKm === undefined ? undefined : [facts.axesKm[0] / facts.radiusKm, facts.axesKm[1] / facts.radiusKm, facts.axesKm[2] / facts.radiusKm]);
57
59
  function featurePaths(facts, pose, frame, stroke, hair) {
58
60
  const f = facts.flattening;
59
- const visible = (lon, lat) => facing(pose, lon, lat, f) > 0.02;
61
+ const axes = axesOf(facts);
62
+ const visible = (lon, lat) => facing(pose, lon, lat, f, axes) > 0.02;
60
63
  const out = [];
61
64
  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));
65
+ const parts = runs(pts.map((p) => surfacePoint(pose, p.lon, p.lat, f, axes)), (_, i) => visible(pts[i].lon, pts[i].lat));
63
66
  for (const part of parts) {
64
67
  const whole = close && part.length === pts.length;
65
68
  if (part.length < 2)
@@ -176,7 +179,7 @@ function cachedOrbit(id, time) {
176
179
  }
177
180
  function bodyGroup(id, pose, frame, o, hair, ringScale) {
178
181
  const facts = BODIES[id];
179
- const sil = silhouette(pose, facts.flattening);
182
+ const sil = silhouette(pose, facts.flattening, axesOf(facts));
180
183
  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
184
  const rings = ringPaths(facts, pose, frame, o.stroke, hair, ringScale);
182
185
  const dark = id === "sun" || facts.kind === "star" ? "" : nightPath(pose, frame);
@@ -227,7 +230,7 @@ export function lineSvg(id, options = {}) {
227
230
  if (options.moons === true && facts.parent === undefined && id !== "sun")
228
231
  return systemSvg(id, options, size, stroke, paper, hair, head);
229
232
  const pose = poseFor(id, options);
230
- const reach = Math.max(ringReach(facts), id === "sun" ? 1.08 : 1);
233
+ const reach = Math.max(ringReach(facts), id === "sun" ? 1.08 : 1, ...(axesOf(facts) ?? []));
231
234
  const fit = options.fill === undefined ? 0.94 / reach : options.fill;
232
235
  const zoom = options.zoom ?? 1;
233
236
  const frame = { cx: (size / 2) * (1 - (options.panX ?? 0)), cy: (size / 2) * (1 + (options.panY ?? 0)), r: (size / 2) * fit * zoom };
@@ -108,7 +108,7 @@ export interface Resolved {
108
108
  }
109
109
  export declare const ASPECTS: readonly (readonly [string, number])[];
110
110
  export declare const MAX_ITEMS = 40;
111
- /** The Sun, then each planet followed by its moons nearest first: the order a layout and a row of buttons use. */
111
+ /** The Sun, then each planet and dwarf planet followed by its moons nearest first: the order a layout and a row of buttons use. */
112
112
  export declare const ORDER: readonly BodyId[];
113
113
  export declare const rankOf: (id: BodyId) => number;
114
114
  /**
@@ -285,7 +285,7 @@ export interface Preset {
285
285
  scene: Portrait;
286
286
  }
287
287
  /**
288
- * Ready-made scenes to start from: a family photo of the Sun and the planets, the gas giants to scale, Jupiter and the Galileans, and the Earth and the Moon to scale.
288
+ * Ready-made scenes to start from: a family photo of the Sun and the planets, the gas giants to scale, Jupiter and the Galileans, the Earth and the Moon to scale (size and distance), Mars and its moons at their true distance, and the inner planets round the Sun.
289
289
  *
290
290
  * @example
291
291
  * ```ts
@@ -1,11 +1,12 @@
1
1
  // The portrait builder's scene as plain data, and everything that can be worked out from it without a screen: where each body goes when the layout is automatic, the camera that fits them,
2
2
  // which body is under a pointer, where a dragged body snaps, the order they are drawn in, the history for undo, and the short text that keeps a scene in the address. Pure: no DOM, no three.js.
3
3
  import { ORBITS } from "../data/orbits.js";
4
- import { BODIES, MOONS_OF, PLANET_IDS, ringReach } from "../facts.js";
4
+ import { heliocentric, moonVector } from "../ephemeris.js";
5
+ import { BODIES, DWARF_IDS, MOONS_OF, PLANET_IDS, ringReach } from "../facts.js";
5
6
  export const ASPECTS = [["16:9", 16 / 9], ["4:3", 4 / 3], ["1:1", 1], ["3:4", 3 / 4]];
6
7
  export const MAX_ITEMS = 40;
7
- /** The Sun, then each planet followed by its moons nearest first: the order a layout and a row of buttons use. */
8
- export const ORDER = ["sun", ...PLANET_IDS.flatMap((planet) => [planet, ...(planet in MOONS_OF ? [...MOONS_OF[planet]].sort(byDistance) : [])])];
8
+ /** The Sun, then each planet and dwarf planet followed by its moons nearest first: the order a layout and a row of buttons use. */
9
+ export const ORDER = ["sun", ...[...PLANET_IDS, ...DWARF_IDS].flatMap((planet) => [planet, ...(planet in MOONS_OF ? [...MOONS_OF[planet]].sort(byDistance) : [])])];
9
10
  function byDistance(a, b) {
10
11
  return (ORBITS[a]?.a ?? 0) - (ORBITS[b]?.a ?? 0);
11
12
  }
@@ -122,6 +123,130 @@ function arrangeUnits(units, how, aspect) {
122
123
  }
123
124
  return out;
124
125
  }
126
+ // --- the true orbit distance layout ---------------------------------------------------------------------------------------------------------------------
127
+ const AU_KM = 149_597_870.7;
128
+ const J2000 = Date.UTC(2000, 0, 1, 12);
129
+ /**
130
+ * The orbit layout, in kilometres: the Sun (if it is in the scene) with the planets at their true distance, each planet with its moons at theirs; or each planet with its moons, side by side.
131
+ * With a date the bodies are where they really are, seen from above the ecliptic; without one they are in a line to the right of their centre. Sizes are true, or squeezed so the smallest can be seen.
132
+ */
133
+ function orbitLayout(p) {
134
+ const items = p.items;
135
+ const ms = p.date === undefined ? undefined : Date.parse(p.date);
136
+ const when = ms !== undefined && Number.isFinite(ms) ? ms : undefined;
137
+ const index = new Map(items.map((i, k) => [i.body, k]));
138
+ const planetKind = (id) => BODIES[id].parent === undefined && id !== "sun";
139
+ const planetSpot = (id) => {
140
+ const v = heliocentric(id, when ?? J2000);
141
+ return when === undefined ? { x: Math.hypot(v[0], v[1], v[2]) * AU_KM, y: 0 } : { x: v[0] * AU_KM, y: v[1] * AU_KM };
142
+ };
143
+ const moonSpot = (id) => {
144
+ if (when === undefined)
145
+ return { x: ORBITS[id]?.a ?? 0, y: 0 };
146
+ const v = moonVector(id, when);
147
+ return { x: v[0], y: v[1] };
148
+ };
149
+ const systems = [];
150
+ const taken = new Set();
151
+ const sun = index.get("sun");
152
+ const attachMoons = (system, planet, at) => {
153
+ for (const [k, item] of items.entries()) {
154
+ if (BODIES[item.body].parent !== items[planet].body || taken.has(k))
155
+ continue;
156
+ const m = moonSpot(item.body);
157
+ system.members.push(k);
158
+ system.spot.set(k, { x: at.x + m.x, y: at.y + m.y });
159
+ system.centre.set(k, planet);
160
+ taken.add(k);
161
+ }
162
+ };
163
+ if (sun !== undefined) {
164
+ const system = { members: [sun], spot: new Map([[sun, { x: 0, y: 0 }]]), centre: new Map() };
165
+ taken.add(sun);
166
+ for (const [k, item] of items.entries()) {
167
+ if (!planetKind(item.body))
168
+ continue;
169
+ const at = planetSpot(item.body);
170
+ system.members.push(k);
171
+ system.spot.set(k, at);
172
+ system.centre.set(k, sun);
173
+ taken.add(k);
174
+ attachMoons(system, k, at);
175
+ }
176
+ systems.push(system);
177
+ }
178
+ for (const [k, item] of items.entries()) {
179
+ if (taken.has(k) || !planetKind(item.body))
180
+ continue;
181
+ const system = { members: [k], spot: new Map([[k, { x: 0, y: 0 }]]), centre: new Map() };
182
+ taken.add(k);
183
+ if (p.moons === "auto")
184
+ attachMoons(system, k, { x: 0, y: 0 });
185
+ systems.push(system);
186
+ }
187
+ for (const [k] of items.entries()) {
188
+ if (taken.has(k))
189
+ continue;
190
+ systems.push({ members: [k], spot: new Map([[k, { x: 0, y: 0 }]]), centre: new Map() });
191
+ taken.add(k);
192
+ }
193
+ // sizes: true, or squeezed against the size of the picture the distances make
194
+ const ids = items.map((i) => i.body);
195
+ let span = 0;
196
+ for (const system of systems) {
197
+ for (const k of system.members) {
198
+ const c = system.centre.get(k);
199
+ if (c === undefined)
200
+ continue;
201
+ const a = system.spot.get(k);
202
+ const b = system.spot.get(c);
203
+ span = Math.max(span, Math.hypot(a.x - b.x, a.y - b.y));
204
+ }
205
+ }
206
+ const biggest = Math.max(...ids.map((id) => BODIES[id].radiusKm));
207
+ span = Math.max(span, biggest * 8);
208
+ const radius = (id) => (p.scale === "true" ? BODIES[id].radiusKm : Math.max(0.014 * span, 0.06 * span * (BODIES[id].radiusKm / biggest) ** 0.4));
209
+ const raw = items.map((item, index2) => ({ ...item, index: index2, x: 0, y: 0, r: radius(item.body), reach: radius(item.body) * reachOf(item.body) }));
210
+ // the circle each body goes round its centre on reaches this far every way
211
+ const extent = (system) => {
212
+ let minX = Infinity;
213
+ let maxX = -Infinity;
214
+ let minY = Infinity;
215
+ let maxY = -Infinity;
216
+ for (const k of system.members) {
217
+ const a = system.spot.get(k);
218
+ const reach = raw[k].reach;
219
+ minX = Math.min(minX, a.x - reach);
220
+ maxX = Math.max(maxX, a.x + reach);
221
+ minY = Math.min(minY, a.y - reach);
222
+ maxY = Math.max(maxY, a.y + reach);
223
+ const c = system.centre.get(k);
224
+ if (c !== undefined) {
225
+ const b = system.spot.get(c);
226
+ const d = Math.hypot(a.x - b.x, a.y - b.y);
227
+ minX = Math.min(minX, b.x - d);
228
+ maxX = Math.max(maxX, b.x + d);
229
+ minY = Math.min(minY, b.y - d);
230
+ maxY = Math.max(maxY, b.y + d);
231
+ }
232
+ }
233
+ return { minX, maxX, minY, maxY };
234
+ };
235
+ const extents = systems.map(extent);
236
+ const units = systems.map((s, k) => ({ members: s.members, at: [], minX: extents[k].minX, maxX: extents[k].maxX, halfH: Math.max(extents[k].maxY, -extents[k].minY) }));
237
+ const spots = arrangeUnits(units, "row", p.aspect);
238
+ const reachPoints = [];
239
+ systems.forEach((s, k) => {
240
+ const dy = (extents[k].minY + extents[k].maxY) / 2;
241
+ for (const m of s.members) {
242
+ const a = s.spot.get(m);
243
+ raw[m].x = spots[k].ox + a.x;
244
+ raw[m].y = a.y - dy;
245
+ }
246
+ reachPoints.push({ x: spots[k].ox + extents[k].minX, y: extents[k].minY - dy }, { x: spots[k].ox + extents[k].maxX, y: extents[k].maxY - dy });
247
+ });
248
+ return { placed: raw, reachPoints };
249
+ }
125
250
  /** The camera that holds every body with a margin: its centre and half-height. */
126
251
  export function fitCamera(placed, aspect, margin = 1.1, labels = false) {
127
252
  if (placed.length === 0)
@@ -161,6 +286,12 @@ export function resolve(scene) {
161
286
  if (p.layout === "manual" || p.items.length === 0) {
162
287
  return { placed: p.items.map((item, index) => ({ ...item, index, reach: item.r * reachOf(item.body) })), camera: p.camera };
163
288
  }
289
+ if (p.arrange === "orbit") {
290
+ const { placed: raw, reachPoints } = orbitLayout(p);
291
+ const cam = fitCamera([...raw, ...reachPoints.map((q) => ({ x: q.x, y: q.y, reach: 0 }))], p.aspect, 1.1, p.labels);
292
+ const k = 1 / cam.h;
293
+ return { placed: raw.map((it) => ({ ...it, x: (it.x - cam.x) * k, y: (it.y - cam.y) * k, r: it.r * k, reach: it.reach * k })), camera: { x: 0, y: 0, h: 1 } };
294
+ }
164
295
  const ids = p.items.map((item) => item.body);
165
296
  const sizes = sizesFor(ids, p.scale);
166
297
  const grouped = groups(p.items, p.moons);
@@ -561,7 +692,7 @@ export function decodeScene(text) {
561
692
  }
562
693
  const fromList = (ids, change = {}) => ids.reduce((p, id) => addBody(p, id), { ...defaultPortrait(), ...change });
563
694
  /**
564
- * Ready-made scenes to start from: a family photo of the Sun and the planets, the gas giants to scale, Jupiter and the Galileans, and the Earth and the Moon to scale.
695
+ * Ready-made scenes to start from: a family photo of the Sun and the planets, the gas giants to scale, Jupiter and the Galileans, the Earth and the Moon to scale (size and distance), Mars and its moons at their true distance, and the inner planets round the Sun.
565
696
  *
566
697
  * @example
567
698
  * ```ts
@@ -574,5 +705,7 @@ export const PRESETS = [
574
705
  { id: "family", name: { en: "Family photo", ja: "家族写真" }, scene: fromList(["sun", ...PLANET_IDS], { arrange: "arc", scale: "readable", moons: "manual", orbits: false }) },
575
706
  { id: "giants", name: { en: "Gas giants", ja: "巨大ガス惑星" }, scene: fromList(["jupiter", "saturn", "uranus", "neptune"], { arrange: "row", scale: "true", moons: "manual", bg: "gradient" }) },
576
707
  { id: "galileans", name: { en: "Jupiter and the Galileans", ja: "木星とガリレオ衛星" }, scene: fromList(["jupiter", "io", "europa", "ganymede", "callisto"], { arrange: "row", scale: "readable", moons: "auto", orbits: true }) },
577
- { id: "earthmoon", name: { en: "Earth and Moon to scale", ja: "地球と月 (大きさの比)" }, scene: fromList(["earth", "moon"], { arrange: "row", scale: "true", moons: "auto", orbits: true }) },
708
+ { id: "earthmoon", name: { en: "Earth and Moon to scale", ja: "地球と月 (大きさと距離の比)" }, scene: fromList(["earth", "moon"], { arrange: "orbit", scale: "true", moons: "auto", orbits: true }) },
709
+ { id: "marsmoons", name: { en: "Mars and its moons at true distance", ja: "火星と衛星 (本当の距離)" }, scene: fromList(["mars", "phobos", "deimos"], { arrange: "orbit", scale: "readable", moons: "auto", orbits: true }) },
710
+ { id: "orrery", name: { en: "The inner planets round the Sun", ja: "太陽と内側の惑星 (本当の距離)" }, scene: fromList(["sun", "mercury", "venus", "earth", "mars"], { arrange: "orbit", scale: "readable", moons: "auto", orbits: true, bg: "black" }) },
578
711
  ];
@@ -64,8 +64,9 @@ export function shapesOf(p, resolved, width, height, lang = "en", edit) {
64
64
  const out = { circles: [], lines: [], texts: [] };
65
65
  const byBody = new Map(resolved.placed.map((q) => [q.body, q]));
66
66
  if (p.orbits) {
67
+ const sun = p.arrange === "orbit" ? byBody.get("sun") : undefined;
67
68
  for (const moon of resolved.placed) {
68
- const host = BODIES[moon.body].parent === undefined ? undefined : byBody.get(BODIES[moon.body].parent);
69
+ const host = BODIES[moon.body].parent === undefined ? (moon.body === "sun" ? undefined : sun) : byBody.get(BODIES[moon.body].parent);
69
70
  if (host === undefined)
70
71
  continue;
71
72
  const d = Math.hypot(moon.x - host.x, moon.y - host.y) * at.scale;
package/dist/pose.d.ts CHANGED
@@ -50,12 +50,14 @@ export declare const drawnTilt: (facts: BodyFacts) => number;
50
50
  * ```
51
51
  */
52
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;
53
+ /** The semi-axes of a body that is not a spheroid, as fractions of its radius: along its longitude 0, along its east and along its pole. */
54
+ export type Axes = readonly [number, number, number];
55
+ /** 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; `axes` shape a triaxial body instead. */
56
+ export declare function surfacePoint(pose: Pose, lon: number, lat: number, flattening?: number, axes?: Axes): Vec3;
55
57
  /** 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): {
58
+ export declare function facing(pose: Pose, lon: number, lat: number, flattening?: number, axes?: Axes): number;
59
+ /** 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. With `axes` it is the outline of a triaxial body: its long half-width, its short one and the angle of the long one from straight right, clockwise. */
60
+ export declare function silhouette(pose: Pose, flattening: number, axes?: Axes): {
59
61
  a: number;
60
62
  b: number;
61
63
  angle: number;
package/dist/pose.js CHANGED
@@ -43,26 +43,40 @@ export function portrait(id, options = {}) {
43
43
  const light = unit([-Math.sin(phase) * Math.cos(lift), Math.sin(lift), Math.cos(phase) * Math.cos(lift)]);
44
44
  return { pole, meridian, light };
45
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) {
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; `axes` shape a triaxial body instead. */
47
+ export function surfacePoint(pose, lon, lat, flattening = 0, axes) {
48
48
  const cl = Math.cos(lat * RAD);
49
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);
50
+ const x = cl * Math.cos(lon * RAD) * (axes?.[0] ?? 1);
51
+ const y = cl * Math.sin(lon * RAD) * (axes?.[1] ?? 1);
52
+ const z = Math.sin(lat * RAD) * (axes?.[2] ?? 1 - flattening);
53
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
54
  }
55
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) {
56
+ export function facing(pose, lon, lat, flattening = 0, axes) {
57
57
  const cl = Math.cos(lat * RAD);
58
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);
59
+ const x = (cl * Math.cos(lon * RAD)) / (axes?.[0] ?? 1);
60
+ const y = (cl * Math.sin(lon * RAD)) / (axes?.[1] ?? 1);
61
+ const z = Math.sin(lat * RAD) / (axes?.[2] ?? 1 - flattening);
62
62
  return pose.meridian[2] * x + east[2] * y + pose.pole[2] * z;
63
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) {
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. With `axes` it is the outline of a triaxial body: its long half-width, its short one and the angle of the long one from straight right, clockwise. */
65
+ export function silhouette(pose, flattening, axes) {
66
+ if (axes !== undefined) {
67
+ const east = cross(pose.pole, pose.meridian);
68
+ const cols = [
69
+ [pose.meridian[0] * axes[0], pose.meridian[1] * axes[0]],
70
+ [east[0] * axes[1], east[1] * axes[1]],
71
+ [pose.pole[0] * axes[2], pose.pole[1] * axes[2]],
72
+ ];
73
+ const sxx = cols.reduce((t, c) => t + c[0] * c[0], 0);
74
+ const syy = cols.reduce((t, c) => t + c[1] * c[1], 0);
75
+ const sxy = cols.reduce((t, c) => t + c[0] * c[1], 0);
76
+ const mean = (sxx + syy) / 2;
77
+ const spread = Math.sqrt(((sxx - syy) / 2) ** 2 + sxy * sxy);
78
+ return { a: Math.sqrt(mean + spread), b: Math.sqrt(Math.max(0, mean - spread)), angle: -0.5 * Math.atan2(2 * sxy, sxx - syy) * DEG };
79
+ }
66
80
  const c = 1 - flattening;
67
81
  const s = pose.pole[2];
68
82
  return { a: 1, b: Math.sqrt(c * c * (1 - s * s) + s * s), angle: Math.atan2(pose.pole[0], pose.pole[1]) * DEG };
@@ -1,6 +1,6 @@
1
1
  export declare const VERTEX = "\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying vec3 vW;\nvoid main() {\n vObj = vec3(position.z, position.x, position.y);\n vN = normalize(normalMatrix * normal);\n vec4 w = modelMatrix * vec4(position, 1.0);\n vW = w.xyz;\n gl_Position = projectionMatrix * viewMatrix * w;\n}";
2
2
  /** Noise, craters and the band palette, for the surface code of a body to call. */
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}";
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}\n// Noise that knows how big a pixel is: an octave finer than the pixel can show is replaced by its average (0.5) instead of being sampled, so a small body is smooth and not a spray of pixel-sized specks.\nfloat fbm(vec3 p) { float fw = length(fwidth(p)); float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * mix(0.5, vnoise(p), 1.0 - smoothstep(0.3, 0.7, fw)); p = p * 2.03 + vec3(1.7, 9.2, 3.1); fw *= 2.03; 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. A crater smaller than about two pixels fades out (it would only be noise).\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 float fw = length(fwidth(q));\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 * smoothstep(1.0, 1.08, d) : 0.0;\n h += on * amp * rad * (bowl + rim) * (1.0 - smoothstep(0.35, 0.9, fw / rad));\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 * smoothstep(1.0, 1.08, d);\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
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. */
@@ -19,4 +19,4 @@ export declare const RING_FRAGMENT_WORLD: string;
19
19
  export declare const GLOW_VERTEX = "\nvarying vec2 vP;\nvoid main() { vP = position.xy; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }";
20
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}";
21
21
  /** A cheap lit ball for a moon seen small beside its planet. */
22
- export declare const BALL_FRAGMENT = "\nprecision highp float;\nuniform vec3 uLight;\nuniform vec3 uColour;\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying vec3 vW;\n\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}\nvoid main() {\n vec3 n = normalize(vN);\n float shade = 0.82 + 0.3 * (fbm(normalize(vObj) * 4.0) - 0.5);\n float diff = max(dot(n, uLight), 0.0);\n vec3 c = pow(uColour * shade, vec3(2.2)) * (0.04 + 1.1 * diff);\n gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), 1.0);\n}";
22
+ export declare const BALL_FRAGMENT = "\nprecision highp float;\nuniform vec3 uLight;\nuniform vec3 uColour;\nvarying vec3 vObj;\nvarying vec3 vN;\nvarying vec3 vW;\n\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}\n// Noise that knows how big a pixel is: an octave finer than the pixel can show is replaced by its average (0.5) instead of being sampled, so a small body is smooth and not a spray of pixel-sized specks.\nfloat fbm(vec3 p) { float fw = length(fwidth(p)); float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * mix(0.5, vnoise(p), 1.0 - smoothstep(0.3, 0.7, fw)); p = p * 2.03 + vec3(1.7, 9.2, 3.1); fw *= 2.03; 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. A crater smaller than about two pixels fades out (it would only be noise).\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 float fw = length(fwidth(q));\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 * smoothstep(1.0, 1.08, d) : 0.0;\n h += on * amp * rad * (bowl + rim) * (1.0 - smoothstep(0.35, 0.9, fw / rad));\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 * smoothstep(1.0, 1.08, d);\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}\nvoid main() {\n vec3 n = normalize(vN);\n float shade = 0.82 + 0.3 * (fbm(normalize(vObj) * 4.0) - 0.5);\n float diff = max(dot(n, uLight), 0.0);\n vec3 c = pow(uColour * shade, vec3(2.2)) * (0.04 + 1.1 * diff);\n gl_FragColor = vec4(pow(c, vec3(1.0 / 2.2)), 1.0);\n}";
@@ -19,10 +19,12 @@ float vnoise(vec3 p) {
19
19
  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),
20
20
  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);
21
21
  }
22
- float 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; }
23
- // Craters: a bowl and a raised rim in each cell that has one. Returns a height, negative in a bowl.
22
+ // Noise that knows how big a pixel is: an octave finer than the pixel can show is replaced by its average (0.5) instead of being sampled, so a small body is smooth and not a spray of pixel-sized specks.
23
+ float fbm(vec3 p) { float fw = length(fwidth(p)); float a = 0.5; float s = 0.0; for (int i = 0; i < 5; i++) { s += a * mix(0.5, vnoise(p), 1.0 - smoothstep(0.3, 0.7, fw)); p = p * 2.03 + vec3(1.7, 9.2, 3.1); fw *= 2.03; a *= 0.5; } return s; }
24
+ // Craters: a bowl and a raised rim in each cell that has one. Returns a height, negative in a bowl. A crater smaller than about two pixels fades out (it would only be noise).
24
25
  float craters(vec3 p, float freq, float amp, float seed) {
25
26
  vec3 q = p * freq + seed; vec3 ip = floor(q); float h = 0.0;
27
+ float fw = length(fwidth(q));
26
28
  for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++) {
27
29
  vec3 c = ip + vec3(float(x), float(y), float(z));
28
30
  float on = step(0.52, hash13(c + 3.7));
@@ -30,8 +32,8 @@ float craters(vec3 p, float freq, float amp, float seed) {
30
32
  float rad = mix(0.12, 0.36, hash13(c + 17.3));
31
33
  float d = length(q - ctr) / rad;
32
34
  float bowl = d < 1.0 ? (d * d - 1.0) : 0.0;
33
- float rim = d >= 1.0 ? exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 : 0.0;
34
- h += on * amp * rad * (bowl + rim);
35
+ float rim = d >= 1.0 ? exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 * smoothstep(1.0, 1.08, d) : 0.0;
36
+ h += on * amp * rad * (bowl + rim) * (1.0 - smoothstep(0.35, 0.9, fw / rad));
35
37
  }
36
38
  return h;
37
39
  }
@@ -42,7 +44,7 @@ vec3 dirOf(float lonDeg, float latDeg) { float lo = radians(lonDeg); float la =
42
44
  // One big round bowl with a rim, by longitude, latitude and angular radius in degrees. Negative inside.
43
45
  float bowl(vec3 p, float lonDeg, float latDeg, float radDeg, float depth) {
44
46
  float d = acos(clamp(dot(p, dirOf(lonDeg, latDeg)), -1.0, 1.0)) / radians(radDeg);
45
- return d < 1.0 ? depth * (d * d - 1.0) : depth * exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3;
47
+ return d < 1.0 ? depth * (d * d - 1.0) : depth * exp(-pow((d - 1.0) * 5.0, 2.0)) * 0.3 * smoothstep(1.0, 1.08, d);
46
48
  }
47
49
  // A soft oval on the body, 1 inside and 0 outside, by longitude and latitude in degrees.
48
50
  float oval(vec3 p, float lon0, float lat0, float rx, float ry, float soft) {
@@ -90,7 +92,9 @@ void main() {
90
92
  float hx = dFdx(s.a); float hy = dFdy(s.a);
91
93
  vec3 grad = cross(dpy, n) * hx + cross(n, dpx) * hy;
92
94
  float det = dot(dpx, cross(dpy, n));
93
- vec3 nb = normalize(abs(det) * n - sign(det) * uRelief * grad * 22.0);
95
+ // relief is worked out from differences between neighbouring pixels, which cannot show a bump only a few pixels wide: on a small disc it fades out and the colour alone carries the craters
96
+ float reliefLod = 1.0 - smoothstep(0.008, 0.024, length(fwidth(p)));
97
+ vec3 nb = normalize(abs(det) * n - sign(det) * uRelief * reliefLod * grad * 22.0);
94
98
  float d = dot(nb, uLight);
95
99
  float diff = clamp((d + uWrap) / (1.0 + uWrap), 0.0, 1.0) * smoothstep(-0.02 - uWrap * 0.6, 0.1 + uWrap * 0.8, dg);
96
100
  if (uRingOn > 0.5) {
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file