@johnmorrisdotca/wakusei-planets 0.6.0 → 0.11.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 +49 -0
- package/NOTICE.md +16 -1
- package/README.md +19 -4
- package/dist/bodies/all.d.ts +2 -0
- package/dist/bodies/all.js +2 -0
- package/dist/bodies/dwarfs.d.ts +1 -0
- package/dist/bodies/dwarfs.js +31 -0
- package/dist/bodies/moons-dwarf.d.ts +1 -0
- package/dist/bodies/moons-dwarf.js +6 -0
- package/dist/bodies/moons-neptune.js +1 -0
- package/dist/bodies/moons-pluto.js +1 -1
- package/dist/bodies/moons-saturn.js +3 -2
- package/dist/data/dwarf.d.ts +16 -0
- package/dist/data/dwarf.js +24 -0
- package/dist/data/orbits.d.ts +2 -2
- package/dist/data/orbits.js +4 -0
- package/dist/element.d.ts +4 -2
- package/dist/element.js +4 -0
- package/dist/embed.d.ts +35 -1
- package/dist/embed.js +78 -37
- package/dist/ephemeris.d.ts +6 -6
- package/dist/ephemeris.js +33 -12
- package/dist/facts.d.ts +44 -6
- package/dist/facts.js +32 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/line.js +7 -4
- package/dist/portrait/element.d.ts +77 -0
- package/dist/portrait/element.js +545 -0
- package/dist/portrait/model.d.ts +297 -0
- package/dist/portrait/model.js +711 -0
- package/dist/portrait/overlay.d.ts +50 -0
- package/dist/portrait/overlay.js +173 -0
- package/dist/portrait/pose.d.ts +13 -0
- package/dist/portrait/pose.js +31 -0
- package/dist/portrait/scene3d.d.ts +38 -0
- package/dist/portrait/scene3d.js +123 -0
- package/dist/pose.d.ts +7 -5
- package/dist/pose.js +25 -11
- package/dist/scene/body.d.ts +1 -1
- package/dist/scene/glsl.js +10 -1
- package/dist/scene/painter.d.ts +7 -0
- package/dist/scene/painter.js +9 -1
- package/dist/textures/ceres-1024.webp +0 -0
- package/dist/textures/ceres-512.webp +0 -0
- package/dist/textures/charon-1024.webp +0 -0
- package/dist/textures/charon-512.webp +0 -0
- package/dist/textures/dione-1024.webp +0 -0
- package/dist/textures/dione-512.webp +0 -0
- package/dist/textures/iapetus-1024.webp +0 -0
- package/dist/textures/iapetus-512.webp +0 -0
- package/dist/textures/tethys-1024.webp +0 -0
- package/dist/textures/tethys-512.webp +0 -0
- package/dist/textures/triton-1024.webp +0 -0
- package/dist/textures/triton-512.webp +0 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +18 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Look } from "../facts.ts";
|
|
2
|
+
import { type Background, type Guide, type Portrait, type Resolved } from "./model.ts";
|
|
3
|
+
export interface Shapes {
|
|
4
|
+
circles: {
|
|
5
|
+
cx: number;
|
|
6
|
+
cy: number;
|
|
7
|
+
r: number;
|
|
8
|
+
kind: "orbit" | "select";
|
|
9
|
+
}[];
|
|
10
|
+
lines: {
|
|
11
|
+
x1: number;
|
|
12
|
+
y1: number;
|
|
13
|
+
x2: number;
|
|
14
|
+
y2: number;
|
|
15
|
+
}[];
|
|
16
|
+
texts: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
text: string;
|
|
20
|
+
size: number;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
export interface Star {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
/** Radius as a fraction of the picture's height. */
|
|
27
|
+
r: number;
|
|
28
|
+
a: number;
|
|
29
|
+
}
|
|
30
|
+
/** The sky's stars: the same ones every time, so a picture and its export agree. */
|
|
31
|
+
export declare function starfield(count?: number): Star[];
|
|
32
|
+
/** The CSS behind the picture for a background (the stars are separate gradients, so the page draws them without a script). */
|
|
33
|
+
export declare function backgroundCss(bg: Background): string;
|
|
34
|
+
/** Paint the background onto a 2D canvas the size of the picture. */
|
|
35
|
+
export declare function paintBackground(ctx: CanvasRenderingContext2D, bg: Background, width: number, height: number): void;
|
|
36
|
+
/** The orbit rings and the names for a picture of this size, and the selected body's outline and the guides when asked. */
|
|
37
|
+
export declare function shapesOf(p: Portrait, resolved: Resolved, width: number, height: number, lang?: "en" | "ja", edit?: {
|
|
38
|
+
selected?: number;
|
|
39
|
+
guides?: readonly Guide[];
|
|
40
|
+
}): Shapes;
|
|
41
|
+
/** The shapes as SVG elements (no wrapper), in the picture's pixels. */
|
|
42
|
+
export declare function shapesSvg(s: Shapes): string;
|
|
43
|
+
/** The shapes on a 2D canvas. */
|
|
44
|
+
export declare function drawShapes(ctx: CanvasRenderingContext2D, s: Shapes, ratio?: number): void;
|
|
45
|
+
/**
|
|
46
|
+
* The portrait in the line look: one SVG, the bodies drawn one over another in the order of the list, each as a nested picture sized so its disc has the radius the layout gave it.
|
|
47
|
+
* Anything a body's rings or glow need past the disc is inside its own picture.
|
|
48
|
+
*/
|
|
49
|
+
export declare function lineScene(p: Portrait, resolved: Resolved, width: number, height: number, poses: readonly import("../ephemeris.ts").Pose[], lang?: "en" | "ja"): string;
|
|
50
|
+
export type { Look };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// What is drawn over and under the bodies of a portrait, as plain shapes: the sky behind them (a few hundred stars, always the same ones), the rings of the moons' orbits, the names, and, while
|
|
2
|
+
// editing, the chosen body's outline and the guides. The same shapes become an SVG overlay on the page and 2D canvas strokes in an exported picture, so the two always agree. Pure: no DOM.
|
|
3
|
+
import { BODIES } from "../facts.js";
|
|
4
|
+
import { lineSvg } from "../line.js";
|
|
5
|
+
import { toScreen } from "./model.js";
|
|
6
|
+
/** The sky's stars: the same ones every time, so a picture and its export agree. */
|
|
7
|
+
export function starfield(count = 140) {
|
|
8
|
+
let seed = 20261103;
|
|
9
|
+
const next = () => {
|
|
10
|
+
seed = (seed * 1664525 + 1013904223) >>> 0;
|
|
11
|
+
return seed / 4294967296;
|
|
12
|
+
};
|
|
13
|
+
return Array.from({ length: count }, () => {
|
|
14
|
+
const big = next() > 0.9;
|
|
15
|
+
return { x: next(), y: next(), r: big ? 0.0016 : 0.0008 + next() * 0.0006, a: 0.35 + next() * 0.65 };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const STARS = starfield();
|
|
19
|
+
/** The CSS behind the picture for a background (the stars are separate gradients, so the page draws them without a script). */
|
|
20
|
+
export function backgroundCss(bg) {
|
|
21
|
+
if (bg === "transparent")
|
|
22
|
+
return "transparent";
|
|
23
|
+
if (bg === "gradient")
|
|
24
|
+
return "radial-gradient(120% 140% at 30% 20%, #1c3358 0%, #0d1a33 55%, #050914 100%)";
|
|
25
|
+
if (bg === "black")
|
|
26
|
+
return "#000";
|
|
27
|
+
const dots = STARS.map((s) => `radial-gradient(${(s.r * 1000).toFixed(1)}px ${(s.r * 1000).toFixed(1)}px at ${(s.x * 100).toFixed(2)}% ${(s.y * 100).toFixed(2)}%, rgba(255,255,255,${s.a.toFixed(2)}), transparent)`);
|
|
28
|
+
return `${dots.join(",")},radial-gradient(100% 120% at 50% 40%, #07101f 0%, #000 100%)`;
|
|
29
|
+
}
|
|
30
|
+
/** Paint the background onto a 2D canvas the size of the picture. */
|
|
31
|
+
export function paintBackground(ctx, bg, width, height) {
|
|
32
|
+
if (bg === "transparent")
|
|
33
|
+
return;
|
|
34
|
+
if (bg === "gradient") {
|
|
35
|
+
const g = ctx.createRadialGradient(width * 0.3, height * 0.2, 0, width * 0.3, height * 0.2, Math.max(width, height) * 1.1);
|
|
36
|
+
g.addColorStop(0, "#1c3358");
|
|
37
|
+
g.addColorStop(0.55, "#0d1a33");
|
|
38
|
+
g.addColorStop(1, "#050914");
|
|
39
|
+
ctx.fillStyle = g;
|
|
40
|
+
ctx.fillRect(0, 0, width, height);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (bg === "black") {
|
|
44
|
+
ctx.fillStyle = "#000";
|
|
45
|
+
ctx.fillRect(0, 0, width, height);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const g = ctx.createRadialGradient(width / 2, height * 0.4, 0, width / 2, height * 0.4, Math.max(width, height) * 0.8);
|
|
49
|
+
g.addColorStop(0, "#07101f");
|
|
50
|
+
g.addColorStop(1, "#000");
|
|
51
|
+
ctx.fillStyle = g;
|
|
52
|
+
ctx.fillRect(0, 0, width, height);
|
|
53
|
+
for (const s of STARS) {
|
|
54
|
+
ctx.fillStyle = `rgba(255,255,255,${s.a})`;
|
|
55
|
+
ctx.beginPath();
|
|
56
|
+
ctx.arc(s.x * width, s.y * height, Math.max(0.5, s.r * 1000), 0, Math.PI * 2);
|
|
57
|
+
ctx.fill();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const labelSize = (width) => Math.max(11, Math.min(15, width / 60));
|
|
61
|
+
/** The orbit rings and the names for a picture of this size, and the selected body's outline and the guides when asked. */
|
|
62
|
+
export function shapesOf(p, resolved, width, height, lang = "en", edit) {
|
|
63
|
+
const at = toScreen(resolved.camera, width, height);
|
|
64
|
+
const out = { circles: [], lines: [], texts: [] };
|
|
65
|
+
const byBody = new Map(resolved.placed.map((q) => [q.body, q]));
|
|
66
|
+
if (p.orbits) {
|
|
67
|
+
const sun = p.arrange === "orbit" ? byBody.get("sun") : undefined;
|
|
68
|
+
for (const moon of resolved.placed) {
|
|
69
|
+
const host = BODIES[moon.body].parent === undefined ? (moon.body === "sun" ? undefined : sun) : byBody.get(BODIES[moon.body].parent);
|
|
70
|
+
if (host === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
const d = Math.hypot(moon.x - host.x, moon.y - host.y) * at.scale;
|
|
73
|
+
if (d > 2)
|
|
74
|
+
out.circles.push({ cx: at.px(host.x), cy: at.py(host.y), r: d, kind: "orbit" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (p.labels) {
|
|
78
|
+
const size = labelSize(width);
|
|
79
|
+
const boxes = [];
|
|
80
|
+
for (const q of resolved.placed) {
|
|
81
|
+
const text = BODIES[q.body].name[lang];
|
|
82
|
+
const w = [...text].reduce((n, c) => n + (c.charCodeAt(0) > 255 ? size : size * 0.58), 0);
|
|
83
|
+
const x = at.px(q.x);
|
|
84
|
+
const y = at.py(q.y) + Math.max(q.r * at.scale, 2) + size * 1.25;
|
|
85
|
+
const box = { x0: x - w / 2, x1: x + w / 2, y0: y - size, y1: y + size * 0.3 };
|
|
86
|
+
if (box.x0 < 0 || box.x1 > width || box.y1 > height + size)
|
|
87
|
+
continue;
|
|
88
|
+
if (boxes.some((b) => box.x0 < b.x1 && box.x1 > b.x0 && box.y0 < b.y1 && box.y1 > b.y0))
|
|
89
|
+
continue;
|
|
90
|
+
boxes.push(box);
|
|
91
|
+
out.texts.push({ x, y, text, size });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (edit?.selected !== undefined) {
|
|
95
|
+
const q = resolved.placed[edit.selected];
|
|
96
|
+
if (q !== undefined)
|
|
97
|
+
out.circles.push({ cx: at.px(q.x), cy: at.py(q.y), r: Math.max(q.r * at.scale, 5) + 5, kind: "select" });
|
|
98
|
+
}
|
|
99
|
+
for (const g of edit?.guides ?? [])
|
|
100
|
+
out.lines.push(g.axis === "x" ? { x1: at.px(g.at), y1: 0, x2: at.px(g.at), y2: height } : { x1: 0, y1: at.py(g.at), x2: width, y2: at.py(g.at) });
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
104
|
+
const f = (n) => (Math.round(n * 100) / 100).toString();
|
|
105
|
+
/** The shapes as SVG elements (no wrapper), in the picture's pixels. */
|
|
106
|
+
export function shapesSvg(s) {
|
|
107
|
+
const circles = s.circles.map((c) => (c.kind === "orbit" ? `<circle cx="${f(c.cx)}" cy="${f(c.cy)}" r="${f(c.r)}" fill="none" stroke="#9fb4d6" stroke-opacity="0.45" stroke-width="1"/>` : `<circle cx="${f(c.cx)}" cy="${f(c.cy)}" r="${f(c.r)}" fill="none" stroke="#6ab7ff" stroke-width="2" stroke-dasharray="5 4"/>`));
|
|
108
|
+
const lines = s.lines.map((l) => `<line x1="${f(l.x1)}" y1="${f(l.y1)}" x2="${f(l.x2)}" y2="${f(l.y2)}" stroke="#ff5fa8" stroke-width="1" stroke-dasharray="4 3"/>`);
|
|
109
|
+
const texts = s.texts.map((t) => `<text x="${f(t.x)}" y="${f(t.y)}" font-size="${f(t.size)}" text-anchor="middle" font-family="system-ui,sans-serif" fill="#f2f6ff" stroke="#050914" stroke-width="3" stroke-opacity="0.8" paint-order="stroke" stroke-linejoin="round">${esc(t.text)}</text>`);
|
|
110
|
+
return [...circles, ...lines, ...texts].join("");
|
|
111
|
+
}
|
|
112
|
+
/** The shapes on a 2D canvas. */
|
|
113
|
+
export function drawShapes(ctx, s, ratio = 1) {
|
|
114
|
+
ctx.save();
|
|
115
|
+
ctx.scale(ratio, ratio);
|
|
116
|
+
for (const c of s.circles) {
|
|
117
|
+
ctx.beginPath();
|
|
118
|
+
ctx.arc(c.cx, c.cy, c.r, 0, Math.PI * 2);
|
|
119
|
+
if (c.kind === "orbit") {
|
|
120
|
+
ctx.strokeStyle = "rgba(159,180,214,0.45)";
|
|
121
|
+
ctx.lineWidth = 1;
|
|
122
|
+
ctx.setLineDash([]);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
ctx.strokeStyle = "#6ab7ff";
|
|
126
|
+
ctx.lineWidth = 2;
|
|
127
|
+
ctx.setLineDash([5, 4]);
|
|
128
|
+
}
|
|
129
|
+
ctx.stroke();
|
|
130
|
+
}
|
|
131
|
+
ctx.setLineDash([4, 3]);
|
|
132
|
+
ctx.strokeStyle = "#ff5fa8";
|
|
133
|
+
ctx.lineWidth = 1;
|
|
134
|
+
for (const l of s.lines) {
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.moveTo(l.x1, l.y1);
|
|
137
|
+
ctx.lineTo(l.x2, l.y2);
|
|
138
|
+
ctx.stroke();
|
|
139
|
+
}
|
|
140
|
+
ctx.setLineDash([]);
|
|
141
|
+
ctx.textAlign = "center";
|
|
142
|
+
ctx.lineJoin = "round";
|
|
143
|
+
for (const t of s.texts) {
|
|
144
|
+
ctx.font = `${t.size}px system-ui, sans-serif`;
|
|
145
|
+
ctx.lineWidth = 3;
|
|
146
|
+
ctx.strokeStyle = "rgba(5,9,20,0.8)";
|
|
147
|
+
ctx.strokeText(t.text, t.x, t.y);
|
|
148
|
+
ctx.fillStyle = "#f2f6ff";
|
|
149
|
+
ctx.fillText(t.text, t.x, t.y);
|
|
150
|
+
}
|
|
151
|
+
ctx.restore();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The portrait in the line look: one SVG, the bodies drawn one over another in the order of the list, each as a nested picture sized so its disc has the radius the layout gave it.
|
|
155
|
+
* Anything a body's rings or glow need past the disc is inside its own picture.
|
|
156
|
+
*/
|
|
157
|
+
export function lineScene(p, resolved, width, height, poses, lang = "en") {
|
|
158
|
+
const at = toScreen(resolved.camera, width, height);
|
|
159
|
+
const dark = p.bg !== "transparent";
|
|
160
|
+
const stroke = dark ? undefined : "#33415c";
|
|
161
|
+
const parts = [];
|
|
162
|
+
resolved.placed.forEach((q, i) => {
|
|
163
|
+
const facts = BODIES[q.body];
|
|
164
|
+
const reach = Math.max(facts.rings === undefined ? 1 : Math.max(...facts.rings.map((b) => b.to)) / facts.radiusKm, q.body === "sun" ? 1.08 : 1);
|
|
165
|
+
const fill = 0.94 / reach;
|
|
166
|
+
const rPx = Math.max(q.r * at.scale, 1.2);
|
|
167
|
+
const side = (2 * rPx) / fill;
|
|
168
|
+
const inner = lineSvg(q.body, { size: 480, fill, labels: false, pose: poses[i], lang, ...(stroke !== undefined ? { stroke } : {}) });
|
|
169
|
+
const nested = inner.replace(/^<svg [^>]*>/, () => `<svg x="${f(at.px(q.x) - side / 2)}" y="${f(at.py(q.y) - side / 2)}" width="${f(side)}" height="${f(side)}" viewBox="0 0 480 480" role="img" aria-label="${esc(facts.name[lang])}" overflow="hidden">`);
|
|
170
|
+
parts.push(nested);
|
|
171
|
+
});
|
|
172
|
+
return parts.join("");
|
|
173
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Pose } from "../ephemeris.ts";
|
|
2
|
+
import { type BodyId } from "../facts.ts";
|
|
3
|
+
import { type Item } from "./model.ts";
|
|
4
|
+
/** The pitch a body is drawn at when none is chosen: a little over the pole, more for a body with rings. */
|
|
5
|
+
export declare const defaultPitch: (id: BodyId) => number;
|
|
6
|
+
/**
|
|
7
|
+
* The pole and the prime meridian of a body in the picture's frame (x right, y up, z toward the viewer), after `seconds` of its own turning.
|
|
8
|
+
* With a date the body is turned as the Earth sees it then, and the item's `spin` is how far it is turned from that (from the longitude its portrait shows).
|
|
9
|
+
*/
|
|
10
|
+
export declare function poseOf(item: Pick<Item, "body" | "spin" | "tilt" | "pitch" | "turn">, ms: number | undefined, seconds: number, light: {
|
|
11
|
+
az: number;
|
|
12
|
+
el: number;
|
|
13
|
+
}): Pose;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// How one body of a portrait is turned: from its own spin, tilt and pitch, or from the sky at a date, then carried forward by its own turning. Pure.
|
|
2
|
+
import { apparent } from "../ephemeris.js";
|
|
3
|
+
import { BODIES } from "../facts.js";
|
|
4
|
+
import { portrait } from "../pose.js";
|
|
5
|
+
import { turn } from "../vec.js";
|
|
6
|
+
import { lightVector } from "./model.js";
|
|
7
|
+
/** The pitch a body is drawn at when none is chosen: a little over the pole, more for a body with rings. */
|
|
8
|
+
export const defaultPitch = (id) => (BODIES[id].rings !== undefined ? 22 : 12);
|
|
9
|
+
/**
|
|
10
|
+
* The pole and the prime meridian of a body in the picture's frame (x right, y up, z toward the viewer), after `seconds` of its own turning.
|
|
11
|
+
* With a date the body is turned as the Earth sees it then, and the item's `spin` is how far it is turned from that (from the longitude its portrait shows).
|
|
12
|
+
*/
|
|
13
|
+
export function poseOf(item, ms, seconds, light) {
|
|
14
|
+
const facts = BODIES[item.body];
|
|
15
|
+
const toSun = lightVector(light);
|
|
16
|
+
let pole;
|
|
17
|
+
let meridian;
|
|
18
|
+
if (ms !== undefined) {
|
|
19
|
+
const sky = apparent(item.body, ms);
|
|
20
|
+
pole = sky.pole;
|
|
21
|
+
meridian = turn(sky.meridian, pole, -(item.spin - (facts.showLon ?? 0)));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
const base = portrait(item.body, { spin: item.spin, pitch: item.pitch ?? defaultPitch(item.body), ...(item.tilt !== undefined ? { tilt: item.tilt } : {}) });
|
|
25
|
+
pole = base.pole;
|
|
26
|
+
meridian = base.meridian;
|
|
27
|
+
}
|
|
28
|
+
if (item.turn !== 0 && seconds !== 0)
|
|
29
|
+
meridian = turn(meridian, pole, item.turn * seconds);
|
|
30
|
+
return { pole, meridian, light: toSun };
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Item3D, Scene3D } from "@johnmorrisdotca/kazoku-ui/renderer";
|
|
2
|
+
import type { ThreeParts } from "../scene/body.ts";
|
|
3
|
+
import "../bodies/all.ts";
|
|
4
|
+
import { type Placed, type Resolved } from "./model.ts";
|
|
5
|
+
/** Everything that decides how the scene looks at one draw. */
|
|
6
|
+
export interface PortraitDraw {
|
|
7
|
+
placed: readonly Placed[];
|
|
8
|
+
camera: Resolved["camera"];
|
|
9
|
+
light: {
|
|
10
|
+
az: number;
|
|
11
|
+
el: number;
|
|
12
|
+
};
|
|
13
|
+
look: "realistic" | "painted";
|
|
14
|
+
/** The moment the sky is shown at, in milliseconds since 1970, or `undefined` for a portrait. */
|
|
15
|
+
ms: number | undefined;
|
|
16
|
+
/** Seconds of each body's own turning so far. */
|
|
17
|
+
seconds: number;
|
|
18
|
+
shadows: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface PortraitScene extends Scene3D {
|
|
21
|
+
apply(state: PortraitDraw): void;
|
|
22
|
+
/** Call `fn` right after the next draw, in the same task, while the canvas still holds the picture (it does not keep it after). */
|
|
23
|
+
afterDraw(fn: () => void): void;
|
|
24
|
+
/** The paintings the scene holds now. */
|
|
25
|
+
readonly bodies: number;
|
|
26
|
+
}
|
|
27
|
+
export interface PortraitItemOptions {
|
|
28
|
+
/** A key that changes whenever the picture would: the renderer keeps one still for each key. */
|
|
29
|
+
key: string;
|
|
30
|
+
state(): PortraitDraw;
|
|
31
|
+
textureBase(): string;
|
|
32
|
+
farBase(): string;
|
|
33
|
+
redraw(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The item the shared renderer shows for a portrait: its `build` makes the scene, whose `apply` is handed each state.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createPortraitItem(options: PortraitItemOptions): Item3D<ThreeParts>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createPainter } from "../scene/painter.js";
|
|
2
|
+
import "../bodies/all.js";
|
|
3
|
+
import { lightVector } from "./model.js";
|
|
4
|
+
import { poseOf } from "./pose.js";
|
|
5
|
+
/** The smallest a body is drawn, in CSS pixels across its radius, so a body at true scale is still a dot you can see. */
|
|
6
|
+
const MIN_RADIUS_PX = 1.2;
|
|
7
|
+
const occludersFor = (placed, index) => {
|
|
8
|
+
const me = placed[index];
|
|
9
|
+
const list = [];
|
|
10
|
+
placed.forEach((other, j) => {
|
|
11
|
+
if (j === index || other.body === "sun" || other.r <= 0)
|
|
12
|
+
return;
|
|
13
|
+
const dx = (other.x - me.x) / me.r;
|
|
14
|
+
const dy = (other.y - me.y) / me.r;
|
|
15
|
+
const rr = other.r / me.r;
|
|
16
|
+
const d2 = dx * dx + dy * dy;
|
|
17
|
+
if (d2 > 3600 || rr < 0.01)
|
|
18
|
+
return;
|
|
19
|
+
list.push({ d2, angle: (rr * rr) / Math.max(d2, 1e-6), o: [dx, dy, 0, rr] });
|
|
20
|
+
});
|
|
21
|
+
return list.sort((a, b) => b.angle - a.angle).slice(0, 6).map((l) => l.o);
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The item the shared renderer shows for a portrait: its `build` makes the scene, whose `apply` is handed each state.
|
|
25
|
+
*/
|
|
26
|
+
export function createPortraitItem(options) {
|
|
27
|
+
return {
|
|
28
|
+
key: options.key,
|
|
29
|
+
build({ three: T, width, height }) {
|
|
30
|
+
const painter = createPainter({ textureBase: options.textureBase(), farBase: options.farBase() });
|
|
31
|
+
const scene = new T.Scene();
|
|
32
|
+
const camera = new T.OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
|
|
33
|
+
const held = new Map();
|
|
34
|
+
let viewWidth = width;
|
|
35
|
+
let viewHeight = height;
|
|
36
|
+
let latest = options.state();
|
|
37
|
+
let after = [];
|
|
38
|
+
const ratio = () => Math.min(2, globalThis.devicePixelRatio || 1);
|
|
39
|
+
const drop = (id) => {
|
|
40
|
+
const one = held.get(id);
|
|
41
|
+
if (one === undefined)
|
|
42
|
+
return;
|
|
43
|
+
scene.remove(one.painting.object);
|
|
44
|
+
one.painting.dispose();
|
|
45
|
+
held.delete(id);
|
|
46
|
+
};
|
|
47
|
+
const apply = (state) => {
|
|
48
|
+
latest = state;
|
|
49
|
+
const { placed, camera: cam } = state;
|
|
50
|
+
const present = new Set(placed.map((p) => p.body));
|
|
51
|
+
for (const id of [...held.keys()])
|
|
52
|
+
if (!present.has(id) || held.get(id).look !== state.look)
|
|
53
|
+
drop(id);
|
|
54
|
+
const aspect = viewWidth / Math.max(1, viewHeight);
|
|
55
|
+
const halfH = cam.h;
|
|
56
|
+
const halfW = halfH * aspect;
|
|
57
|
+
const pxPerUnit = viewHeight / (2 * halfH);
|
|
58
|
+
const slot = 7 * Math.max(1e-6, ...placed.map((p) => Math.max(p.r, MIN_RADIUS_PX / pxPerUnit)));
|
|
59
|
+
const far = (placed.length + 1) * slot;
|
|
60
|
+
const distance = far + 300 * Math.max(halfW, halfH);
|
|
61
|
+
camera.left = cam.x - halfW;
|
|
62
|
+
camera.right = cam.x + halfW;
|
|
63
|
+
camera.top = cam.y + halfH;
|
|
64
|
+
camera.bottom = cam.y - halfH;
|
|
65
|
+
camera.near = Math.max(1e-6, distance - far - slot);
|
|
66
|
+
camera.far = distance + slot;
|
|
67
|
+
camera.position.set(cam.x, cam.y, distance);
|
|
68
|
+
camera.updateProjectionMatrix();
|
|
69
|
+
const toSun = lightVector(state.light);
|
|
70
|
+
placed.forEach((p, i) => {
|
|
71
|
+
let one = held.get(p.body);
|
|
72
|
+
if (one === undefined) {
|
|
73
|
+
const painting = painter.make(T, p.body, { look: state.look, invalidate: () => options.redraw(), ratio: ratio(), maxTexture: placed.length > 6 ? 1024 : 2048 });
|
|
74
|
+
scene.add(painting.object);
|
|
75
|
+
one = { painting, look: state.look };
|
|
76
|
+
held.set(p.body, one);
|
|
77
|
+
}
|
|
78
|
+
const r = Math.max(p.r, MIN_RADIUS_PX / pxPerUnit);
|
|
79
|
+
one.painting.object.position.set(p.x, p.y, i * slot);
|
|
80
|
+
one.painting.object.scale.setScalar(r);
|
|
81
|
+
const pose = poseOf(p, state.ms, state.seconds, state.light);
|
|
82
|
+
one.painting.update({
|
|
83
|
+
toSun,
|
|
84
|
+
ms: state.ms ?? 0,
|
|
85
|
+
radiusPx: r * pxPerUnit,
|
|
86
|
+
pose: { pole: pose.pole, meridian: pose.meridian },
|
|
87
|
+
...(state.shadows ? { occluders: occludersFor(placed, i) } : {}),
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
apply(latest);
|
|
92
|
+
return {
|
|
93
|
+
scene,
|
|
94
|
+
camera,
|
|
95
|
+
apply,
|
|
96
|
+
get bodies() {
|
|
97
|
+
return held.size;
|
|
98
|
+
},
|
|
99
|
+
afterDraw(fn) {
|
|
100
|
+
after.push(fn);
|
|
101
|
+
},
|
|
102
|
+
resize(w, h) {
|
|
103
|
+
viewWidth = w;
|
|
104
|
+
viewHeight = h;
|
|
105
|
+
apply(latest);
|
|
106
|
+
},
|
|
107
|
+
update() {
|
|
108
|
+
if (after.length > 0) {
|
|
109
|
+
const run = after;
|
|
110
|
+
after = [];
|
|
111
|
+
queueMicrotask(() => run.forEach((fn) => fn()));
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
},
|
|
115
|
+
dispose() {
|
|
116
|
+
for (const id of [...held.keys()])
|
|
117
|
+
drop(id);
|
|
118
|
+
scene.clear();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
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
|
-
/**
|
|
54
|
-
export
|
|
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 };
|
package/dist/scene/body.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Pose } from "../ephemeris.ts";
|
|
|
4
4
|
import { type BodyFacts, type BodyId } from "../facts.ts";
|
|
5
5
|
import { type BodyLook } from "./look.ts";
|
|
6
6
|
/** The parts of three.js the bodies use: a page may hand over a module with only these (see `setThreeLoader`). */
|
|
7
|
-
export type ThreeParts = Pick<typeof THREE, "AdditiveBlending" | "BufferAttribute" | "BufferGeometry" | "ClampToEdgeWrapping" | "DataTexture" | "DoubleSide" | "Line" | "LineBasicMaterial" | "LinearFilter" | "Mesh" | "NoColorSpace" | "OrthographicCamera" | "PlaneGeometry" | "RepeatWrapping" | "RGBAFormat" | "RingGeometry" | "Scene" | "ShaderMaterial" | "SphereGeometry" | "TextureLoader" | "Vector2" | "Vector3" | "WebGLRenderer">;
|
|
7
|
+
export type ThreeParts = Pick<typeof THREE, "AdditiveBlending" | "BufferAttribute" | "BufferGeometry" | "ClampToEdgeWrapping" | "DataTexture" | "DoubleSide" | "Group" | "Line" | "LineBasicMaterial" | "LinearFilter" | "Mesh" | "NoColorSpace" | "OrthographicCamera" | "PlaneGeometry" | "RepeatWrapping" | "RGBAFormat" | "RingGeometry" | "Scene" | "ShaderMaterial" | "SphereGeometry" | "TextureLoader" | "Vector2" | "Vector3" | "WebGLRenderer">;
|
|
8
8
|
/** One moon in a system view: its place in planet radii (viewer's frame) and the points of its orbit, if they changed. */
|
|
9
9
|
export interface MoonDraw {
|
|
10
10
|
id: BodyId;
|
package/dist/scene/glsl.js
CHANGED
|
@@ -71,7 +71,7 @@ ${painted ? "uniform sampler2D uMap;" : ""}
|
|
|
71
71
|
varying vec3 vObj;
|
|
72
72
|
varying vec3 vN;
|
|
73
73
|
varying vec3 vW;
|
|
74
|
-
${world ? "varying vec3 vV;" : ""}
|
|
74
|
+
${world ? "varying vec3 vV;\nuniform int uOccN;\nuniform vec4 uOcc[6];" : ""}
|
|
75
75
|
${PRELUDE}
|
|
76
76
|
${surface}
|
|
77
77
|
void main() {
|
|
@@ -104,6 +104,15 @@ void main() {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
${world ? ` for (int i = 0; i < 6; i++) {
|
|
108
|
+
if (i >= uOccN) break;
|
|
109
|
+
vec3 oc = uOcc[i].xyz - vW;
|
|
110
|
+
float along = dot(oc, uLight);
|
|
111
|
+
if (along > 0.0) {
|
|
112
|
+
float miss = length(oc - along * uLight);
|
|
113
|
+
diff *= 1.0 - 0.92 * (1.0 - smoothstep(uOcc[i].w * 0.8, uOcc[i].w * 1.04 + 0.01, miss));
|
|
114
|
+
}
|
|
115
|
+
}` : ""}
|
|
107
116
|
vec3 alb = pow(s.rgb, vec3(2.2));
|
|
108
117
|
vec3 lit = alb * (uAmbient + (1.0 - uAmbient) * diff * 1.18);
|
|
109
118
|
float rim = pow(1.0 - mu, uAtmosPow);
|
package/dist/scene/painter.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export interface PaintInfo {
|
|
|
63
63
|
toSun: Vec3;
|
|
64
64
|
ms: number;
|
|
65
65
|
radiusPx: number;
|
|
66
|
+
/** The pole and the prime meridian to draw instead of the body's real ones at `ms` (a portrait's own pose), in the scene's axes. */
|
|
67
|
+
pose?: {
|
|
68
|
+
pole: Vec3;
|
|
69
|
+
meridian: Vec3;
|
|
70
|
+
};
|
|
71
|
+
/** Spheres that may shade this body from the Sun: x, y, z (from this body's centre) and radius, all in this body's radii, in the scene's axes. At most six are used. */
|
|
72
|
+
occluders?: readonly (readonly [number, number, number, number])[];
|
|
66
73
|
}
|
|
67
74
|
/**
|
|
68
75
|
* One body made by the painter: an object of radius 1 at the origin for the caller to move and scale.
|
package/dist/scene/painter.js
CHANGED
|
@@ -95,6 +95,8 @@ function paint(T, id, context, bases) {
|
|
|
95
95
|
uRingTex: { value: null },
|
|
96
96
|
uMap: { value: null },
|
|
97
97
|
uScale: { value: 1 },
|
|
98
|
+
uOccN: { value: 0 },
|
|
99
|
+
uOcc: { value: new Float32Array(24) },
|
|
98
100
|
};
|
|
99
101
|
};
|
|
100
102
|
const material = (painted) => {
|
|
@@ -280,17 +282,23 @@ function paint(T, id, context, bases) {
|
|
|
280
282
|
light.set(info.toSun[0], info.toSun[1], info.toSun[2]);
|
|
281
283
|
const scale = group.scale.x || 1;
|
|
282
284
|
if (look.emissive !== true) {
|
|
283
|
-
const { pole, meridian } = orientation(id, info.ms);
|
|
285
|
+
const { pole, meridian } = info.pose ?? orientation(id, info.ms);
|
|
284
286
|
const east = cross(pole, meridian);
|
|
285
287
|
basis(mesh, east, pole, meridian, scaleX, scaleY, scaleZ);
|
|
286
288
|
if (ringMesh !== undefined)
|
|
287
289
|
basis(ringMesh, meridian, east, pole);
|
|
288
290
|
poleVec.set(pole[0], pole[1], pole[2]);
|
|
289
291
|
}
|
|
292
|
+
const occluders = info.occluders ?? [];
|
|
293
|
+
const shaded = Math.min(6, occluders.length);
|
|
290
294
|
for (const m of materials) {
|
|
291
295
|
m.uniforms.uLight.value.copy(light);
|
|
292
296
|
m.uniforms.uScale.value = scale;
|
|
293
297
|
m.uniforms.uRingN.value.copy(poleVec);
|
|
298
|
+
m.uniforms.uOccN.value = shaded;
|
|
299
|
+
const packed = m.uniforms.uOcc.value;
|
|
300
|
+
for (let i = 0; i < shaded; i += 1)
|
|
301
|
+
packed.set(occluders[i], i * 4);
|
|
294
302
|
}
|
|
295
303
|
if (ringMaterial !== undefined) {
|
|
296
304
|
ringMaterial.uniforms.uLight.value.copy(light);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.11.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.11.0";
|