@johnmorrisdotca/wakusei-planets 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 - 2026-11-03
4
+
5
+ The portrait builder: any bodies, one picture.
6
+
7
+ - **`portrait.html` and `<wakusei-portrait>`** (MODULES-231): put the Sun, the planets, Pluto and any of the 25 moons in one picture. Drag a body to move it (in a manual layout it snaps to the middle and edges of the others, with guide lines; hold Alt to switch the snap off), scroll or pinch over it to size it, turn it (the Turn tool, or Shift and a drag: the surface under the pointer follows the pointer, and the arrow keys agree; a body can also turn by itself, the way it really turns), order the bodies front to back, remove them, undo and redo, Centre and Fit all.
8
+ - **Auto-fit and Manual.** Auto-fit places and sizes the bodies and always fits them in: a row, an arc or a grid, at true scale (every size true to the others) or readable (sizes squeezed so the smallest can be seen), with a planet's moons going round it in orbit order, each with its orbit ring (Moons: round their planet, or on their own). Manual keeps every body where it was put and the size it was given, and moving to Manual keeps what Auto-fit had.
9
+ - **One light, a background, names, orbit rings, shadows, a date.** One Sun direction for the whole picture; a black, starry, gradient or clear background; names and orbit rings on or off; shadows (a body can shade another from the Sun, as in an eclipse: `uOcc` in the shader); the sky at a date, with each body's real pole. Line, realistic or painted for the whole scene. Presets: a family photo, the gas giants, Jupiter and the Galileans, the Earth and Moon to scale.
10
+ - **The scene is in the address** (`?scene=...`; `encodeScene`, `decodeScene`), so a link is the picture. **Share** writes the page code in every way a page is made (`portraitCode`, built on the same code as `embedCode`, so `embed.html?scene=...` is the iframe), saves a PNG, saves an SVG (all vector for the line look; the bodies as a picture inside the SVG for the shaded looks) and copies the link.
11
+ - **Performance.** All the bodies of a scene are in ONE three.js scene on the page's one WebGL context; it draws only on a change, thirty times a second at most while something moves, nothing out of sight; pictures are fetched at the size they need and given back. The freeze test plays twelve bodies in one scene (rest, turning, a drag, changes to the scene, the 5K canvas budget).
12
+ - `painter.make(...).update` takes an optional `pose` (a pole and a prime meridian to draw instead of the real ones) and `occluders` (spheres that shade the body from the Sun). `ThreeParts` now includes `Group`: a page that hands over its own three.js subset adds `Group`.
13
+ - New entry point `@johnmorrisdotca/wakusei-planets/portrait`; `addBody`, `layoutPortrait`, `encodeScene`, `decodeScene`, `PORTRAIT_PRESETS` and `portraitCode` in the main one. A guide, "The portrait builder", in English and Japanese.
14
+
15
+ ## 0.6.0 - 2026-11-03
16
+
17
+ The demo's pickers, and a scale that is a scale.
18
+
19
+ - **A compact picker** (MODULES-285): the ten bodies in one row, and when the one chosen has moons, its moons on the row below, in the same place; no long list down the page. The moons' own section is gone. Every picture in it is a picture, in the line, the realistic and the painted look: a test looks at all 35 (10 bodies and 25 moons) for blank and single-colour pictures. (The broken thumbnails of 0.3.0 were the tiles being drawn to scale, which 0.3.1 stopped.) The same picker is in "Put it in your page" (MODULES-286), so the body for the code is chosen beside the code; Copy's word can be read (it was ink on ink).
20
+ - **The buttons under the view stay where they are** (MODULES-283): one push button for each body of the system (the planet and its moons), the chosen one pressed, and others pressed with it to see them side by side under the view, at equal sizes or at their true relative sizes (`span-km`), kept in the address (`?with=phobos,deimos&rel=1`). The orbit-distance layout is not made yet.
21
+ - **To scale says what it is against** (MODULES-284): "Its planet" (for a moon), "Earth" or "Sun", and draws it so: Deimos is 0.0018 times Mars's radius, a magnified picture beside it with a line, the caption says so. `?scale=1&ref=planet`.
22
+ - **Deimos is drawn whole** (MODULES-287): an irregular body reaches past its mean radius along its longest axis, and its picture was cut off at the sides.
23
+ - **Every body is fitted by its globe** (MODULES-293): at about half the view's height, the same for all, so Saturn's rings, the Sun's glow and a haze have room, and the Sun is no smaller than a planet. `fit="tight"` on an element gives the old fit (thumbnails).
24
+ - **The sky shows the real phase** (MODULES-294): in "As seen from Earth" the night side is dark (no wrapped terminator, no rim of air round it), so Venus a few days from new is a thin crescent, not a full disc.
25
+ - **A line under every choice** (MODULES-298) says what it does, in English and Japanese, always on the page and tied to its button with `aria-describedby`.
26
+
3
27
  ## 0.5.1 - 2026-11-03
4
28
 
5
29
  - 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.
package/README.md CHANGED
@@ -13,9 +13,9 @@ A clean line drawing, a shaded realistic drawing, or a painted surface map, with
13
13
 
14
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>
15
15
 
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>
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.7.0/docs/images/hero-desk.png" width="640"></p>
17
17
 
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>
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.7.0/docs/images/lineup-desk.png" width="640"></p>
19
19
 
20
20
  ## One minute
21
21
 
@@ -271,6 +271,19 @@ A body that turns by itself and a moving sky, in HTML:
271
271
 
272
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
273
 
274
+ ## The portrait builder
275
+
276
+ <p align="center"><img alt="The Sun and the planets in an arc, painted, with their names, in the portrait builder" src="https://raw.githubusercontent.com/johnmorrisdotca/wakusei-planets/v0.7.0/docs/images/portrait-family.png" width="640"></p>
277
+
278
+ [The portrait builder](https://johnmorrisdotca.github.io/wakusei-planets/portrait.html) puts any bodies in one picture: drag them, size them, turn them, order them front to back, arrange them in a row, an arc or a grid (true or readable sizes, a planet's moons round it in orbit order), light them from one direction, and take the code, a PNG or an SVG. The scene is kept in the address, and every body is drawn in one WebGL scene, so the page holds one WebGL context. [The portrait builder](docs/guides/portrait-builder.md) is its guide.
279
+
280
+ ```html
281
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets/portrait/+esm"></script>
282
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets/all/+esm"></script>
283
+
284
+ <wakusei-portrait scene="look:painted~jupiter_0_0_0.3_0~io_0_0_0.1_0"></wakusei-portrait>
285
+ ```
286
+
274
287
  ## API
275
288
 
276
289
  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/).
@@ -279,6 +292,7 @@ The reference is a site, made from the source's own doc comments: every export h
279
292
  | --- | --- |
280
293
  | `@johnmorrisdotca/wakusei-planets` | The pure side: `BODIES`, `apparent`, `system`, `orbitPath`, `portrait`, `lineSvg`, `embedCode`. No browser. |
281
294
  | `.../element` | `<wakusei-body>`; three.js is loaded only for a shaded look. |
295
+ | `.../portrait` | `<wakusei-portrait>`: many bodies in one scene. The scene model (`addBody`, `layoutPortrait`, `encodeScene`, `decodeScene`) is in the main entry. |
282
296
  | `.../look` | What a body needs to be drawn with three.js. |
283
297
  | `.../painter` | The bodies as objects for a scene of your own. |
284
298
  | `.../all` | Every body's shaded look, at once. |
@@ -302,6 +316,7 @@ Each guide has a Japanese twin in [docs/guides/ja](docs/guides/ja/getting-starte
302
316
  | [Frameworks](docs/guides/frameworks.md) | React, Vue, Svelte, Angular |
303
317
  | [Embed in an iframe](docs/guides/embed-in-an-iframe.md) | No script of your own |
304
318
  | [Put it in your page](docs/guides/put-it-in-your-page.md) | Set up a body and copy the code |
319
+ | [The portrait builder](docs/guides/portrait-builder.md) | Many bodies in one picture |
305
320
  | [Looks and bodies](docs/guides/looks-and-bodies.md) | The looks and each body's entry point |
306
321
  | [Moons and time](docs/guides/moons-and-time.md) | The sky on a date, time that runs fast |
307
322
  | [In your own three.js scene](docs/guides/in-your-own-scene.md) | `painter.make` |
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.5.1/dist/textures/";
28
+ export declare const TEXTURE_BASE = "https://cdn.jsdelivr.net/npm/@johnmorrisdotca/wakusei-planets@0.7.0/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.5.1/textures/";
40
+ export declare const TEXTURE_FAR_BASE = "https://cdn.jsdelivr.net/gh/johnmorrisdotca/wakusei-planets@v0.7.0/textures/";
41
41
  /**
42
42
  * The `<wakusei-body>` custom element: a body of the solar system, drawn in a look.
43
43
  *
package/dist/element.js CHANGED
@@ -48,6 +48,8 @@ const readTime = (value) => {
48
48
  return Number.isFinite(ms) ? ms : undefined;
49
49
  };
50
50
  /** The half-width of the picture, in body radii, for a body alone, with a scale to another body, or with its moons. */
51
+ /** The half-width of a body's picture, in its radii, when it has no scale of its own: the globe takes a little under half the height, so its rings, glow and haze have room round it. */
52
+ const ROOM = 2.2;
51
53
  function extentOf(id, options) {
52
54
  const f = BODIES[id];
53
55
  if (options.moons === true && id in BODIES && f.parent === undefined && id !== "sun") {
@@ -59,7 +61,11 @@ function extentOf(id, options) {
59
61
  return options.spanKm / f.radiusKm;
60
62
  if (options.scaleTo !== undefined)
61
63
  return (BODIES[options.scaleTo].radiusKm * 1.06) / f.radiusKm;
62
- return Math.max(id === "sun" ? 1.55 : 1.06, ringReach(f) * 1.04);
64
+ if (options.tight !== true)
65
+ return ROOM;
66
+ // tight (a thumbnail): the picture holds the body and its rings, and an irregular body (Deimos, Phobos, Nix, Hydra) whole along its longest axis
67
+ const longest = f.axesKm === undefined ? 1 : Math.max(...f.axesKm) / f.radiusKm;
68
+ return Math.max(1.06, ringReach(f) * 1.04, longest * 1.06);
63
69
  }
64
70
  /** Keep the body in the picture: at least a quarter of its width stays inside the edge, however far the view is moved. `pan` is in half-widths of the picture. */
65
71
  function clampPan(pan, extent, zoom) {
@@ -91,7 +97,7 @@ const TEMPLATE = `<style>
91
97
  * ```
92
98
  */
93
99
  export class WakuseiBody extends HTMLElement {
94
- static observedAttributes = ["body", "look", "size", "time", "tilt", "pitch", "phase", "meridian", "turn", "rate", "moons", "range", "scale-to", "span-km", "live", "texture-base", "texture-far-base", "lang", "labels"];
100
+ static observedAttributes = ["body", "look", "size", "time", "tilt", "pitch", "phase", "meridian", "turn", "rate", "moons", "range", "scale-to", "span-km", "fit", "live", "texture-base", "texture-far-base", "lang", "labels"];
95
101
  #stage;
96
102
  #state = "line";
97
103
  #renderer;
@@ -293,7 +299,7 @@ export class WakuseiBody extends HTMLElement {
293
299
  const scaleTo = this.getAttribute("scale-to");
294
300
  const range = this.hasAttribute("range") ? num(this.getAttribute("range"), 0) : undefined;
295
301
  const span = num(this.getAttribute("span-km"), 0);
296
- const extent = extentOf(this.body, { ...(span > 0 ? { spanKm: span } : {}), ...(scaleTo !== null && scaleTo in BODIES ? { scaleTo } : {}), moons: r.moons, ...(range !== undefined && range > 0 ? { range } : {}) });
302
+ const extent = extentOf(this.body, { ...(this.getAttribute("fit") === "tight" ? { tight: true } : {}), ...(span > 0 ? { spanKm: span } : {}), ...(scaleTo !== null && scaleTo in BODIES ? { scaleTo } : {}), moons: r.moons, ...(range !== undefined && range > 0 ? { range } : {}) });
297
303
  const moons = r.moons
298
304
  ? frame.moons
299
305
  .filter((m) => m.distance <= extent * 1.05)
@@ -303,7 +309,7 @@ export class WakuseiBody extends HTMLElement {
303
309
  return { id: m.id, x: rotated[0], y: rotated[1], z: rotated[2], ...(orbit !== undefined ? { orbit: tiltBy === 0 ? orbit : this.#tiltedOrbit(m.id, orbit, tiltBy) } : {}) };
304
310
  })
305
311
  : undefined;
306
- return { pose: { pole, meridian, light }, extent, zoom: this.#zoom, panX: clampPan(this.#panX, extent, this.#zoom), panY: clampPan(this.#panY, extent, this.#zoom), ...(moons !== undefined ? { moons } : {}) };
312
+ return { pose: { pole, meridian, light }, extent, sky: frame.time !== undefined, zoom: this.#zoom, panX: clampPan(this.#panX, extent, this.#zoom), panY: clampPan(this.#panY, extent, this.#zoom), ...(moons !== undefined ? { moons } : {}) };
307
313
  }
308
314
  #tilted = new Map();
309
315
  #tiltedOrbit(id, orbit, tilt) {
@@ -442,7 +448,7 @@ export class WakuseiBody extends HTMLElement {
442
448
  size,
443
449
  pose: scene.pose,
444
450
  ...(r.moons ? { moons: true, ...(frame.time !== undefined ? { time: frame.time } : {}), range: scene.extent, moonsAt: (scene.moons ?? []).map((m) => ({ id: m.id, x: m.x, y: m.y, z: m.z, distance: Math.hypot(m.x, m.y, m.z) })), orbits: new Map((scene.moons ?? []).filter((m) => m.orbit !== undefined).map((m) => [m.id, m.orbit])) } : {}),
445
- ...(scaleTo !== null || this.hasAttribute("span-km") ? { fill: this.#fill() } : {}),
451
+ ...(scaleTo !== null || this.hasAttribute("span-km") || this.getAttribute("fit") !== "tight" ? { fill: this.#fill() } : {}),
446
452
  ...(this.getAttribute("lang") === "ja" ? { lang: "ja" } : {}),
447
453
  ...(this.getAttribute("labels") === "off" ? { labels: false } : {}),
448
454
  ...(stroke !== "" ? { stroke } : {}),
@@ -532,7 +538,7 @@ export class WakuseiBody extends HTMLElement {
532
538
  const scaleTo = this.getAttribute("scale-to");
533
539
  const span = num(this.getAttribute("span-km"), 0);
534
540
  const half = span > 0 ? span : scaleTo !== null && scaleTo in BODIES ? BODIES[scaleTo].radiusKm * 1.06 : 0;
535
- return half > 0 ? Math.max(0.01, Math.min(1, BODIES[this.body].radiusKm / half)) : 1;
541
+ return half > 0 ? Math.max(0.01, Math.min(1, BODIES[this.body].radiusKm / half)) : this.getAttribute("fit") === "tight" ? 1 : 1 / ROOM;
536
542
  }
537
543
  /** Make the view's still, once, when it is in sight. */
538
544
  #still() {
package/dist/embed.d.ts CHANGED
@@ -80,6 +80,40 @@ export declare function entryOf(id: BodyId): string;
80
80
  * ```
81
81
  */
82
82
  export declare function embedCode(options: EmbedOptions, kind?: EmbedKind, settings?: EmbedSettings): string;
83
+ /**
84
+ * A scene of the portrait builder to put in a page: the scene as the builder writes it in its address, how wide the picture is, and the look to draw it in if not the scene's own.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { portraitCode, type PortraitEmbed } from "@johnmorrisdotca/wakusei-planets";
89
+ *
90
+ * const options: PortraitEmbed = { scene: "look:painted~jupiter_0_0_0.3_0~io_0_0_0.1_0", width: 640 };
91
+ * console.log(portraitCode(options, "html").includes("wakusei-portrait"));
92
+ * ```
93
+ */
94
+ export interface PortraitEmbed {
95
+ /** The scene as text (`encodeScene`). */
96
+ scene: string;
97
+ /** Pixels across; default 720. */
98
+ width?: number;
99
+ }
100
+ /**
101
+ * The text that puts a portrait scene in a page, for each way a page is made, as `embedCode` does for a single body. The bodies' own entry points are loaded only for the bodies in the scene
102
+ * (all of them when there are more than six kinds), and none for the line look.
103
+ *
104
+ * @param options - The scene and the width.
105
+ * @param kind - The way the page is made; `html` by default.
106
+ * @param settings - The CDN and the demo's address, when they are not the package's own.
107
+ * @returns The text to paste into the page's source.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * import { portraitCode } from "@johnmorrisdotca/wakusei-planets";
112
+ *
113
+ * console.log(portraitCode({ scene: "look:painted~jupiter_0_0_0.3_0~io_0_0_0.1_0" }, "vue"));
114
+ * ```
115
+ */
116
+ export declare function portraitCode(options: PortraitEmbed, kind?: EmbedKind, settings?: EmbedSettings): string;
83
117
  /**
84
118
  * The credits that travel with a picture of a body: the line and shaded looks are drawn from numbers, the painted look uses public-domain pictures.
85
119
  *
package/dist/embed.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // The text to put a body in a page, for each way a page is made: a script tag from a CDN, an ES module, React, Vue, Angular, Svelte, or an iframe of `embed.html`. The demo's "Put it in your page"
2
2
  // section shows these as the reader changes a choice, and the tests build or parse every one. Pure: no DOM.
3
3
  import { BODIES } from "./facts.js";
4
+ import { decodeScene } from "./portrait/model.js";
4
5
  import { VERSION } from "./version.js";
5
6
  /**
6
7
  * The ways `embedCode` writes a body into a page.
@@ -35,8 +36,36 @@ function attributesOf(o) {
35
36
  return out;
36
37
  }
37
38
  const quote = (v) => `"${v.replace(/&/g, "&amp;").replace(/"/g, "&quot;")}"`;
38
- const tagAttrs = (o) => attributesOf(o).map(([k, v]) => (v === "" ? k : `${k}=${quote(v)}`)).join(" ");
39
39
  const needsShading = (o) => (o.look ?? "realistic") !== "line";
40
+ const attrText = (attrs) => attrs.map(([k, v]) => (v === "" ? k : `${k}=${quote(v)}`)).join(" ");
41
+ function snippet(kind, spec, cdn) {
42
+ const Name = spec.name.charAt(0).toUpperCase() + spec.name.slice(1);
43
+ const tagAttrs2 = attrText(spec.attrs);
44
+ if (kind === "iframe") {
45
+ const f = spec.iframe;
46
+ return `<iframe\n src=${quote(f.src)}\n title=${quote(f.title)}\n width="${f.width}"\n height="${f.height}"\n loading="lazy"\n style="border: 0"\n></iframe>`;
47
+ }
48
+ if (kind === "html") {
49
+ const scripts = [`<script type="module" src="${cdn}/${spec.element}/+esm"></script>`, ...spec.entries.map((e) => `<script type="module" src="${cdn}/${e}/+esm"></script>`)];
50
+ return `${scripts.join("\n")}\n\n<${spec.tag} ${tagAttrs2}></${spec.tag}>`;
51
+ }
52
+ const imports = [`import "${PACKAGE}/${spec.element}";`, ...spec.entries.map((e) => `import "${PACKAGE}/${e}";`)];
53
+ if (kind === "module") {
54
+ const sets = spec.attrs.map(([k, v]) => `${spec.name}.setAttribute(${JSON.stringify(k)}, ${JSON.stringify(v)});`);
55
+ return `${imports.join("\n")}\n\nconst ${spec.name} = document.createElement("${spec.tag}");\n${sets.join("\n")}\ndocument.querySelector("#${spec.name}").append(${spec.name});`;
56
+ }
57
+ const dynamic = [`void import("${PACKAGE}/${spec.element}");`, ...spec.entries.map((e) => `void import("${PACKAGE}/${e}");`)];
58
+ if (kind === "react") {
59
+ return `import { useEffect } from "react";\n\nexport function ${Name}() {\n useEffect(() => {\n // The element needs a browser, so it is imported in an effect, never at the top of the file.\n${dynamic.map((l) => ` ${l}`).join("\n")}\n }, []);\n return <${spec.tag} ${tagAttrs2} />;\n}`;
60
+ }
61
+ if (kind === "vue") {
62
+ return `<script setup lang="ts">\nimport { onMounted } from "vue";\n\nonMounted(() => {\n${dynamic.map((l) => ` ${l}`).join("\n")}\n});\n</script>\n\n<template>\n <${spec.tag} ${tagAttrs2}></${spec.tag}>\n</template>`;
63
+ }
64
+ if (kind === "angular") {
65
+ return `import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit } from "@angular/core";\n\n@Component({\n selector: "app-${spec.name}",\n standalone: true,\n schemas: [CUSTOM_ELEMENTS_SCHEMA],\n template: \`<${spec.tag} ${tagAttrs2}></${spec.tag}>\`,\n})\nexport class ${Name}Component implements OnInit {\n ngOnInit(): void {\n // The element needs a browser: ngOnInit also runs when a server renders.\n if (typeof window !== "undefined") {\n${dynamic.map((l) => ` ${l}`).join("\n")}\n }\n }\n}`;
66
+ }
67
+ return `<script lang="ts">\n import { onMount } from "svelte";\n\n onMount(() => {\n${dynamic.map((l) => ` ${l}`).join("\n")}\n });\n</script>\n\n<${spec.tag} ${tagAttrs2}></${spec.tag}>`;
68
+ }
40
69
  /**
41
70
  * The text that puts a body in a page, for one way of making the page. Each way is built, parsed or run by the package's tests: the plain HTML one with a script tag from jsDelivr, an ES module, a React component,
42
71
  * a Vue single-file component, an Angular standalone component, a Svelte component, and an iframe of the demo's `embed.html`.
@@ -56,40 +85,50 @@ const needsShading = (o) => (o.look ?? "realistic") !== "line";
56
85
  export function embedCode(options, kind = "html", settings = {}) {
57
86
  const cdn = settings.cdn ?? CDN;
58
87
  const site = settings.site ?? SITE;
59
- const shaded = needsShading(options);
60
- const entry = entryOf(options.body);
61
88
  const size = options.size ?? 320;
62
- if (kind === "iframe") {
63
- const query = new URLSearchParams({ body: options.body, look: options.look ?? "realistic" });
64
- if (options.time !== undefined)
65
- query.set("time", options.time);
66
- if (options.moons === true && options.time !== undefined)
67
- query.set("moons", "1");
68
- if (options.turn !== undefined && options.turn > 0)
69
- query.set("turn", String(options.turn));
70
- return `<iframe\n src=${quote(`${site}embed.html?${query.toString()}`)}\n title=${quote(BODIES[options.body].name.en)}\n width="${size}"\n height="${size}"\n loading="lazy"\n style="border: 0"\n></iframe>`;
71
- }
72
- if (kind === "html") {
73
- const scripts = [`<script type="module" src="${cdn}/element/+esm"></script>`, ...(shaded ? [`<script type="module" src="${cdn}/${entry}/+esm"></script>`] : [])];
74
- return `${scripts.join("\n")}\n\n<wakusei-body ${tagAttrs(options)}></wakusei-body>`;
75
- }
76
- const imports = [`import "${PACKAGE}/element";`, ...(shaded ? [`import "${PACKAGE}/${entry}";`] : [])];
77
- if (kind === "module") {
78
- const sets = attributesOf(options).map(([k, v]) => `planet.setAttribute(${JSON.stringify(k)}, ${JSON.stringify(v)});`);
79
- return `${imports.join("\n")}\n\nconst planet = document.createElement("wakusei-body");\n${sets.join("\n")}\ndocument.querySelector("#planet").append(planet);`;
80
- }
81
- const dynamic = [`void import("${PACKAGE}/element");`, ...(shaded ? [`void import("${PACKAGE}/${entry}");`] : [])];
82
- if (kind === "react") {
83
- const props = attributesOf(options).map(([k, v]) => (v === "" ? k : `${k}=${quote(v)}`)).join(" ");
84
- return `import { useEffect } from "react";\n\nexport function Planet() {\n useEffect(() => {\n // The element needs a browser, so it is imported in an effect, never at the top of the file.\n${dynamic.map((l) => ` ${l}`).join("\n")}\n }, []);\n return <wakusei-body ${props} />;\n}`;
85
- }
86
- if (kind === "vue") {
87
- return `<script setup lang="ts">\nimport { onMounted } from "vue";\n\nonMounted(() => {\n${dynamic.map((l) => ` ${l}`).join("\n")}\n});\n</script>\n\n<template>\n <wakusei-body ${tagAttrs(options)}></wakusei-body>\n</template>`;
88
- }
89
- if (kind === "angular") {
90
- return `import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit } from "@angular/core";\n\n@Component({\n selector: "app-planet",\n standalone: true,\n schemas: [CUSTOM_ELEMENTS_SCHEMA],\n template: \`<wakusei-body ${tagAttrs(options)}></wakusei-body>\`,\n})\nexport class PlanetComponent implements OnInit {\n ngOnInit(): void {\n // The element needs a browser: ngOnInit also runs when a server renders.\n if (typeof window !== "undefined") {\n${dynamic.map((l) => ` ${l}`).join("\n")}\n }\n }\n}`;
91
- }
92
- return `<script lang="ts">\n import { onMount } from "svelte";\n\n onMount(() => {\n${dynamic.map((l) => ` ${l}`).join("\n")}\n });\n</script>\n\n<wakusei-body ${tagAttrs(options)}></wakusei-body>`;
89
+ const query = new URLSearchParams({ body: options.body, look: options.look ?? "realistic" });
90
+ if (options.time !== undefined)
91
+ query.set("time", options.time);
92
+ if (options.moons === true && options.time !== undefined)
93
+ query.set("moons", "1");
94
+ if (options.turn !== undefined && options.turn > 0)
95
+ query.set("turn", String(options.turn));
96
+ return snippet(kind, {
97
+ tag: "wakusei-body",
98
+ element: "element",
99
+ entries: needsShading(options) ? [entryOf(options.body)] : [],
100
+ attrs: attributesOf(options),
101
+ name: "planet",
102
+ iframe: { src: `${site}embed.html?${query.toString()}`, title: BODIES[options.body].name.en, width: size, height: size },
103
+ }, cdn);
104
+ }
105
+ /**
106
+ * The text that puts a portrait scene in a page, for each way a page is made, as `embedCode` does for a single body. The bodies' own entry points are loaded only for the bodies in the scene
107
+ * (all of them when there are more than six kinds), and none for the line look.
108
+ *
109
+ * @param options - The scene and the width.
110
+ * @param kind - The way the page is made; `html` by default.
111
+ * @param settings - The CDN and the demo's address, when they are not the package's own.
112
+ * @returns The text to paste into the page's source.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { portraitCode } from "@johnmorrisdotca/wakusei-planets";
117
+ *
118
+ * console.log(portraitCode({ scene: "look:painted~jupiter_0_0_0.3_0~io_0_0_0.1_0" }, "vue"));
119
+ * ```
120
+ */
121
+ export function portraitCode(options, kind = "html", settings = {}) {
122
+ const cdn = settings.cdn ?? CDN;
123
+ const site = settings.site ?? SITE;
124
+ const width = options.width ?? 720;
125
+ const scene = decodeScene(options.scene);
126
+ const kinds = [...new Set(scene.items.map((i) => entryOf(i.body)))];
127
+ const entries = scene.look === "line" ? [] : kinds.length > 6 ? ["all"] : kinds;
128
+ const attrs = [["scene", options.scene]];
129
+ if (width !== 720)
130
+ attrs.push(["size", String(width)]);
131
+ return snippet(kind, { tag: "wakusei-portrait", element: "portrait", entries, attrs, name: "portrait", iframe: { src: `${site}embed.html?${new URLSearchParams({ scene: options.scene }).toString()}`, title: "Wakusei Planets portrait", width, height: Math.round(width / scene.aspect) } }, cdn);
93
132
  }
94
133
  /**
95
134
  * The credits that travel with a picture of a body: the line and shaded looks are drawn from numbers, the painted look uses public-domain pictures.
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export { apparent, moonGeocentric, heliocentric, orbitPath, system, type Apparen
3
3
  export { drawnTilt, portrait, type PortraitOptions } from "./pose.ts";
4
4
  export { lineSvg, mix, type LineOptions } from "./line.ts";
5
5
  export { ringStrip, type RingStrip } from "./scene/rings.ts";
6
- export { embedCode, embedCredit, EMBED_KINDS, type EmbedKind, type EmbedOptions, type EmbedSettings } from "./embed.ts";
6
+ export { embedCode, embedCredit, EMBED_KINDS, portraitCode, type EmbedKind, type EmbedOptions, type EmbedSettings, type PortraitEmbed } from "./embed.ts";
7
+ export { addBody, decodeScene, defaultPortrait, encodeScene, PRESETS as PORTRAIT_PRESETS, resolve as layoutPortrait, type Item as PortraitItem, type Placed as PlacedItem, type Portrait, type Preset as PortraitPreset, type Resolved as PortraitLayout } from "./portrait/model.ts";
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ export { apparent, moonGeocentric, heliocentric, orbitPath, system } from "./eph
3
3
  export { drawnTilt, portrait } from "./pose.js";
4
4
  export { lineSvg, mix } from "./line.js";
5
5
  export { ringStrip } from "./scene/rings.js";
6
- export { embedCode, embedCredit, EMBED_KINDS } from "./embed.js";
6
+ export { embedCode, embedCredit, EMBED_KINDS, portraitCode } from "./embed.js";
7
+ export { addBody, decodeScene, defaultPortrait, encodeScene, PRESETS as PORTRAIT_PRESETS, resolve as layoutPortrait } from "./portrait/model.js";
package/dist/line.js CHANGED
@@ -227,7 +227,7 @@ export function lineSvg(id, options = {}) {
227
227
  if (options.moons === true && facts.parent === undefined && id !== "sun")
228
228
  return systemSvg(id, options, size, stroke, paper, hair, head);
229
229
  const pose = poseFor(id, options);
230
- const reach = Math.max(ringReach(facts), id === "sun" ? 1.26 : 1);
230
+ const reach = Math.max(ringReach(facts), id === "sun" ? 1.08 : 1);
231
231
  const fit = options.fill === undefined ? 0.94 / reach : options.fill;
232
232
  const zoom = options.zoom ?? 1;
233
233
  const frame = { cx: (size / 2) * (1 - (options.panX ?? 0)), cy: (size / 2) * (1 + (options.panY ?? 0)), r: (size / 2) * fit * zoom };
@@ -0,0 +1,77 @@
1
+ import type { SharedRenderer } from "@johnmorrisdotca/kazoku-ui/renderer";
2
+ import type { ThreeParts } from "../scene/body.ts";
3
+ import { setThreeLoader } from "../scene/shared.ts";
4
+ import { type Guide, type Placed, type Portrait, type Resolved } from "./model.ts";
5
+ export { setThreeLoader };
6
+ /**
7
+ * What the element is doing: `line` (an SVG), `loading`, `still`, `live`, `lost` or `unavailable` (no WebGL: the line drawing is shown).
8
+ *
9
+ * @example
10
+ * ```ts no-run
11
+ * import type { PortraitState } from "@johnmorrisdotca/wakusei-planets/portrait";
12
+ *
13
+ * const done = (state: PortraitState): boolean => state === "live" || state === "line";
14
+ * console.log(done("still"));
15
+ * ```
16
+ */
17
+ export type PortraitState = "line" | "loading" | "still" | "live" | "lost" | "unavailable";
18
+ /**
19
+ * The `<wakusei-portrait>` custom element: a scene of bodies arranged on one picture.
20
+ *
21
+ * Attributes: `scene` (the scene as the portrait builder writes it in its address), `look` (`line`, `realistic` or `painted`: replaces the scene's), `size` (pixels across), `lang` (`en` or `ja`: the names),
22
+ * `live` (`always`, `hover` or `off`: when it takes the page's one WebGL context), `texture-base` and `texture-far-base` (where the surface pictures are).
23
+ *
24
+ * @example
25
+ * ```ts no-run
26
+ * import { WakuseiPortrait } from "@johnmorrisdotca/wakusei-planets/portrait";
27
+ *
28
+ * const view = document.createElement("wakusei-portrait") as WakuseiPortrait;
29
+ * view.setAttribute("scene", "arrange:arc~look:painted~sun_0_0_0.3_0~earth_0_0_0.3_0~moon_0_0_0.3_0");
30
+ * document.body.append(view);
31
+ * ```
32
+ */
33
+ export declare class WakuseiPortrait extends HTMLElement {
34
+ #private;
35
+ static observedAttributes: string[];
36
+ /** The scene now. Setting it hands the element a scene of your own, as the builder does; it is drawn at once and at most thirty times a second after. */
37
+ get portrait(): Portrait;
38
+ set portrait(value: Portrait);
39
+ /** The chosen body's index and the guides to draw over the picture (the builder's editing). */
40
+ set editing(value: {
41
+ selected?: number;
42
+ guides?: readonly Guide[];
43
+ });
44
+ get state(): PortraitState;
45
+ /** Numbers for a test or a status line. */
46
+ get stats(): {
47
+ state: PortraitState;
48
+ bodies: number;
49
+ renderer: SharedRenderer<ThreeParts>["stats"] | undefined;
50
+ };
51
+ /** Where everything is on the page now: the picture's size in pixels, pixels to a scene unit, and the bodies (back to front). */
52
+ get metrics(): {
53
+ width: number;
54
+ height: number;
55
+ scale: number;
56
+ camera: Resolved["camera"];
57
+ placed: readonly Placed[];
58
+ };
59
+ /** The body under a point of the page (client coordinates): its index in the list, or -1. `slop` is the smallest radius, in pixels, a body is hit within. */
60
+ pick(clientX: number, clientY: number, slop?: number): number;
61
+ /** A point of the page (client coordinates) in the scene's units. */
62
+ worldAt(clientX: number, clientY: number): {
63
+ x: number;
64
+ y: number;
65
+ };
66
+ connectedCallback(): void;
67
+ disconnectedCallback(): void;
68
+ attributeChangedCallback(name: string): void;
69
+ /** Take the page's one WebGL context and draw the scene on it. Resolves true when it is live; a line scene has nothing to take and resolves false. */
70
+ goLive(): Promise<boolean>;
71
+ /** Give the WebGL context back: the picture stays as a still. */
72
+ rest(): void;
73
+ /** The picture as a PNG: the background, the bodies and the names, at the size it is on the page times `scale` (the canvas of the bodies is at most the page's pixel ratio, 2). */
74
+ toPng(scale?: number): Promise<Blob>;
75
+ /** The picture as SVG. The line look is all vector. The shaded looks hold the bodies as a picture inside the SVG, with the background, the orbits and the names as vector. */
76
+ toSvg(): Promise<string>;
77
+ }