entangled-worlds-orbiters-shared 2.10.0 → 2.12.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/README.md +10 -0
- package/dist/notes/index.d.ts +72 -0
- package/dist/notes/index.js +94 -0
- package/dist/star/blackbody.d.ts +12 -0
- package/dist/star/blackbody.js +38 -0
- package/dist/star/bounds.d.ts +39 -0
- package/dist/star/bounds.js +47 -0
- package/dist/star/framing.d.ts +31 -0
- package/dist/star/framing.js +68 -0
- package/dist/star/index.d.ts +21 -0
- package/dist/star/index.js +15 -0
- package/dist/star/layout.d.ts +121 -0
- package/dist/star/layout.js +307 -0
- package/dist/star/lighting.d.ts +65 -0
- package/dist/star/lighting.js +115 -0
- package/dist/star/params.d.ts +79 -0
- package/dist/star/params.js +122 -0
- package/dist/star/quality.d.ts +16 -0
- package/dist/star/quality.js +16 -0
- package/dist/star/shaders.d.ts +20 -0
- package/dist/star/shaders.js +186 -0
- package/dist/star/starBody.d.ts +58 -0
- package/dist/star/starBody.js +308 -0
- package/dist/star/starManager.d.ts +73 -0
- package/dist/star/starManager.js +190 -0
- package/dist/star/stellarData.d.ts +82 -0
- package/dist/star/stellarData.js +347 -0
- package/package.json +17 -4
- package/src/core/index.js +1 -1
- package/src/core/lights.js +17 -1
- package/src/loaders/glbLoader.js +5 -2
- package/src/moons/index.js +11 -1
- package/src/moons/moonsGeneration.js +24 -8
- package/src/moons/moonsManager.js +18 -0
- package/src/notes/index.ts +146 -0
- package/src/star/blackbody.ts +46 -0
- package/src/star/bounds.ts +73 -0
- package/src/star/framing.ts +96 -0
- package/src/star/index.ts +59 -0
- package/src/star/layout.ts +400 -0
- package/src/star/lighting.ts +142 -0
- package/src/star/params.ts +161 -0
- package/src/star/quality.ts +33 -0
- package/src/star/shaders.ts +195 -0
- package/src/star/starBody.ts +385 -0
- package/src/star/starManager.ts +224 -0
- package/src/star/stellarData.ts +387 -0
- package/src/world/cloudShell.js +2 -3
- package/src/world/worldGLB.js +3 -2
- package/src/world/worldManager.js +1 -0
- package/src/world/worldTexture.js +4 -3
package/README.md
CHANGED
|
@@ -71,6 +71,16 @@ import * as EntangledWorlds from 'entangled-worlds-orbiters-shared';
|
|
|
71
71
|
### Configuration
|
|
72
72
|
- `textureConfig` - Texture configuration system
|
|
73
73
|
|
|
74
|
+
### Star (`entangled-worlds-orbiters-shared/star`)
|
|
75
|
+
- `createStar`, `StarManager` - The host star of a world: single, binary or trinary, emissive, shader-only glow (two draw calls for any multiplicity; lights nothing)
|
|
76
|
+
- `DEFAULT_STAR_PARAMS`, `normalizeStarParams` - Author params (temperature, radius, luminosity, activity, multiplicity, separation, seed, placement) + validator for stored params
|
|
77
|
+
- `starParamsFromStellarData` - Catalogue host-star data (Teff, radius, mass, luminosity, spectral type, star count or a "(AB)b" name suffix) → params
|
|
78
|
+
- `blackbodyColor`, `spectralClass`, `starLayout`, `starPositions` - Pure colour / layout / orbit helpers
|
|
79
|
+
- `getFocus` (on `createStar` / `StarManager`), `framingDistance`, `defaultStarPlacement` - Camera focus on the system barycentre and host-camera-aware placement
|
|
80
|
+
- The star lights the scene: `StarManager` key + camera-side fill lights, `setLighting`, `lightingFromLegacy`, `DEFAULT_AMBIENT` (pair with `setHemisphereEnabled(scene, false)` from `./core`; moons `emissiveLift`, GLB `emissiveIntensity` for night sides)
|
|
81
|
+
- Real binaries (close pair from component masses, a distant companion for wide pairs) and an optional debris disc; `mainSequenceTemperature` / `Radius` / `Luminosity` helpers
|
|
82
|
+
- Perf + look harness: `npm run bench:star` (see `bench/star/results/REPORT.md`)
|
|
83
|
+
|
|
74
84
|
### Capabilities (`entangled-worlds-orbiters-shared/capabilities`)
|
|
75
85
|
- `createCapabilityRegistry` - Validated, named registry of studio actions (query / mutation / generative)
|
|
76
86
|
- `validate`, `assertValid` - JSON-Schema-subset validator used for capability inputs
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type NoteEventType = 'on' | 'off';
|
|
2
|
+
export interface NoteEvent {
|
|
3
|
+
type: NoteEventType;
|
|
4
|
+
/** Unique per sounding note; an `off` carries the id of the `on` it ends. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** MIDI note number, 0 … 127 (integers today; the field allows fractions later). */
|
|
7
|
+
pitch: number;
|
|
8
|
+
/** 0 … 1. An `off` may carry a release velocity; nothing reads it yet. */
|
|
9
|
+
velocity: number;
|
|
10
|
+
/** Audio-context seconds when it should sound; at or before now means now. */
|
|
11
|
+
time: number;
|
|
12
|
+
/** Who sent it: a device, the keyboard, a MIDI effect's slot, a room client. */
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const NOTE_PITCH_MIN = 0;
|
|
16
|
+
export declare const NOTE_PITCH_MAX = 127;
|
|
17
|
+
/** Whether `value` is a well-formed note event (the shape a room peer or an agent must send). */
|
|
18
|
+
export declare function isNoteEvent(value: unknown): value is NoteEvent;
|
|
19
|
+
/** What a channel message means to a note player. Channels are 0 … 15, as on the wire. */
|
|
20
|
+
export type MidiNoteMessage = {
|
|
21
|
+
kind: 'on';
|
|
22
|
+
channel: number;
|
|
23
|
+
pitch: number;
|
|
24
|
+
velocity: number;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'off';
|
|
27
|
+
channel: number;
|
|
28
|
+
pitch: number;
|
|
29
|
+
velocity: number;
|
|
30
|
+
}
|
|
31
|
+
/** Pitch bend, −1 … 1 (0 is the centre). */
|
|
32
|
+
| {
|
|
33
|
+
kind: 'bend';
|
|
34
|
+
channel: number;
|
|
35
|
+
value: number;
|
|
36
|
+
}
|
|
37
|
+
/** A control change, its value 0 … 1. */
|
|
38
|
+
| {
|
|
39
|
+
kind: 'cc';
|
|
40
|
+
channel: number;
|
|
41
|
+
controller: number;
|
|
42
|
+
value: number;
|
|
43
|
+
};
|
|
44
|
+
/** The mod wheel. */
|
|
45
|
+
export declare const MIDI_CC_MOD_WHEEL = 1;
|
|
46
|
+
/**
|
|
47
|
+
* Decode a channel message's bytes. A note-on with velocity 0 is a note-off (running status sends
|
|
48
|
+
* them that way). Anything a note player has no use for (aftertouch, program change, system) is null.
|
|
49
|
+
*/
|
|
50
|
+
export declare function decodeMidiMessage(status: number, data1?: number, data2?: number): MidiNoteMessage | null;
|
|
51
|
+
/**
|
|
52
|
+
* A MIDI note-off names only a channel and a pitch. This gives each held key its note id, so the
|
|
53
|
+
* off pairs with its on by id like every other source.
|
|
54
|
+
*
|
|
55
|
+
* - `on` mints a fresh id. If that key was already held (a device that sends two ons without an off)
|
|
56
|
+
* the old note's id comes back as `replaced`, so the caller can end it first: nothing sticks.
|
|
57
|
+
* - `off` returns the id the key was playing, or null when it was never down (an off arriving after
|
|
58
|
+
* a panic, or from before the page listened).
|
|
59
|
+
* - `releaseDevice` hands back every id a device holds: it went away, and they all end.
|
|
60
|
+
*/
|
|
61
|
+
export interface MidiNoteIds {
|
|
62
|
+
on(device: string, channel: number, pitch: number): {
|
|
63
|
+
id: string;
|
|
64
|
+
replaced: string | null;
|
|
65
|
+
};
|
|
66
|
+
off(device: string, channel: number, pitch: number): string | null;
|
|
67
|
+
releaseDevice(device: string): string[];
|
|
68
|
+
releaseAll(): string[];
|
|
69
|
+
/** How many keys are down, across devices. */
|
|
70
|
+
readonly size: number;
|
|
71
|
+
}
|
|
72
|
+
export declare function createMidiNoteIds(prefix?: string): MidiNoteIds;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Notes as events, shared by every note source (MIDI in, the on-screen keyboard, a capability call, a
|
|
2
|
+
// MIDI effect, a room peer) and everything that routes or sounds them.
|
|
3
|
+
//
|
|
4
|
+
// On and off are separate events with no duration: a live key has no known length when it goes
|
|
5
|
+
// down. They pair by `id`, never by pitch, since two sources can hold the same pitch at once and a
|
|
6
|
+
// chord or an echo plays the same pitch more than once. Pairing by pitch alone gives stuck notes.
|
|
7
|
+
//
|
|
8
|
+
// App-agnostic: nothing here knows about Orbiters, dimensions or the audio graph.
|
|
9
|
+
export const NOTE_PITCH_MIN = 0;
|
|
10
|
+
export const NOTE_PITCH_MAX = 127;
|
|
11
|
+
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
12
|
+
/** Whether `value` is a well-formed note event (the shape a room peer or an agent must send). */
|
|
13
|
+
export function isNoteEvent(value) {
|
|
14
|
+
if (!value || typeof value !== 'object')
|
|
15
|
+
return false;
|
|
16
|
+
const event = value;
|
|
17
|
+
return ((event.type === 'on' || event.type === 'off') &&
|
|
18
|
+
typeof event.id === 'string' &&
|
|
19
|
+
event.id.length > 0 &&
|
|
20
|
+
isFiniteNumber(event.pitch) &&
|
|
21
|
+
event.pitch >= NOTE_PITCH_MIN &&
|
|
22
|
+
event.pitch <= NOTE_PITCH_MAX &&
|
|
23
|
+
isFiniteNumber(event.velocity) &&
|
|
24
|
+
event.velocity >= 0 &&
|
|
25
|
+
event.velocity <= 1 &&
|
|
26
|
+
isFiniteNumber(event.time) &&
|
|
27
|
+
typeof event.source === 'string');
|
|
28
|
+
}
|
|
29
|
+
/** The mod wheel. */
|
|
30
|
+
export const MIDI_CC_MOD_WHEEL = 1;
|
|
31
|
+
const BEND_CENTRE = 8192;
|
|
32
|
+
/**
|
|
33
|
+
* Decode a channel message's bytes. A note-on with velocity 0 is a note-off (running status sends
|
|
34
|
+
* them that way). Anything a note player has no use for (aftertouch, program change, system) is null.
|
|
35
|
+
*/
|
|
36
|
+
export function decodeMidiMessage(status, data1 = 0, data2 = 0) {
|
|
37
|
+
const channel = status & 0x0f;
|
|
38
|
+
switch (status & 0xf0) {
|
|
39
|
+
case 0x90:
|
|
40
|
+
return data2 > 0
|
|
41
|
+
? { kind: 'on', channel, pitch: data1 & 0x7f, velocity: (data2 & 0x7f) / 127 }
|
|
42
|
+
: { kind: 'off', channel, pitch: data1 & 0x7f, velocity: 0 };
|
|
43
|
+
case 0x80:
|
|
44
|
+
return { kind: 'off', channel, pitch: data1 & 0x7f, velocity: (data2 & 0x7f) / 127 };
|
|
45
|
+
case 0xe0: {
|
|
46
|
+
// 14 bits, least significant first. The top is 8191 above the centre and the bottom 8192 below
|
|
47
|
+
// it, so each side is scaled on its own and both ends reach ±1.
|
|
48
|
+
const raw = ((data2 & 0x7f) << 7) | (data1 & 0x7f);
|
|
49
|
+
const offset = raw - BEND_CENTRE;
|
|
50
|
+
return { kind: 'bend', channel, value: offset >= 0 ? offset / (BEND_CENTRE - 1) : offset / BEND_CENTRE };
|
|
51
|
+
}
|
|
52
|
+
case 0xb0:
|
|
53
|
+
return { kind: 'cc', channel, controller: data1 & 0x7f, value: (data2 & 0x7f) / 127 };
|
|
54
|
+
default:
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function createMidiNoteIds(prefix = 'midi') {
|
|
59
|
+
const held = new Map();
|
|
60
|
+
let next = 0;
|
|
61
|
+
const keyOf = (device, channel, pitch) => `${device}\u0000${channel}\u0000${pitch}`;
|
|
62
|
+
const deviceOf = (key) => key.slice(0, key.indexOf('\u0000'));
|
|
63
|
+
const take = (predicate) => {
|
|
64
|
+
const ids = [];
|
|
65
|
+
for (const [key, id] of held) {
|
|
66
|
+
if (!predicate(key))
|
|
67
|
+
continue;
|
|
68
|
+
ids.push(id);
|
|
69
|
+
held.delete(key);
|
|
70
|
+
}
|
|
71
|
+
return ids;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
on(device, channel, pitch) {
|
|
75
|
+
const key = keyOf(device, channel, pitch);
|
|
76
|
+
const replaced = held.get(key) ?? null;
|
|
77
|
+
next += 1;
|
|
78
|
+
const id = `${prefix}:${next}`;
|
|
79
|
+
held.set(key, id);
|
|
80
|
+
return { id, replaced };
|
|
81
|
+
},
|
|
82
|
+
off(device, channel, pitch) {
|
|
83
|
+
const key = keyOf(device, channel, pitch);
|
|
84
|
+
const id = held.get(key) ?? null;
|
|
85
|
+
held.delete(key);
|
|
86
|
+
return id;
|
|
87
|
+
},
|
|
88
|
+
releaseDevice: (device) => take((key) => deviceOf(key) === device),
|
|
89
|
+
releaseAll: () => take(() => true),
|
|
90
|
+
get size() {
|
|
91
|
+
return held.size;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RGB {
|
|
2
|
+
r: number;
|
|
3
|
+
g: number;
|
|
4
|
+
b: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* sRGB colour (channels 0..1, brightest channel = 1) of a blackbody at `temperatureK`.
|
|
8
|
+
* Clamped to 1000 K … 40000 K, the range the fit is valid for.
|
|
9
|
+
*/
|
|
10
|
+
export declare function blackbodyColor(temperatureK: number): RGB;
|
|
11
|
+
/** Linear-light luminance-ish weight of an sRGB colour — handy for keeping companions balanced. */
|
|
12
|
+
export declare function relativeLuminance({ r, g, b }: RGB): number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Blackbody temperature → display colour.
|
|
2
|
+
//
|
|
3
|
+
// A fitted approximation of the Planck locus in sRGB (Tanner Helland's curve fit of Mitchell
|
|
4
|
+
// Charity's blackbody table), normalised so the brightest channel is 1. It is a *chromaticity*:
|
|
5
|
+
// brightness is the renderer's job. Pure and cheap — called on param changes, never per frame.
|
|
6
|
+
function clamp01(v) {
|
|
7
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* sRGB colour (channels 0..1, brightest channel = 1) of a blackbody at `temperatureK`.
|
|
11
|
+
* Clamped to 1000 K … 40000 K, the range the fit is valid for.
|
|
12
|
+
*/
|
|
13
|
+
export function blackbodyColor(temperatureK) {
|
|
14
|
+
const tk = Number.isFinite(temperatureK) ? temperatureK : 5778;
|
|
15
|
+
const t = Math.min(40000, Math.max(1000, tk)) / 100;
|
|
16
|
+
let r;
|
|
17
|
+
let g;
|
|
18
|
+
let b;
|
|
19
|
+
if (t <= 66) {
|
|
20
|
+
r = 255;
|
|
21
|
+
g = 99.4708025861 * Math.log(t) - 161.1195681661;
|
|
22
|
+
b = t <= 19 ? 0 : 138.5177312231 * Math.log(t - 10) - 305.0447927307;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
r = 329.698727446 * Math.pow(t - 60, -0.1332047592);
|
|
26
|
+
g = 288.1221695283 * Math.pow(t - 60, -0.0755148492);
|
|
27
|
+
b = 255;
|
|
28
|
+
}
|
|
29
|
+
r = clamp01(r / 255);
|
|
30
|
+
g = clamp01(g / 255);
|
|
31
|
+
b = clamp01(b / 255);
|
|
32
|
+
const m = Math.max(r, g, b) || 1;
|
|
33
|
+
return { r: r / m, g: g / m, b: b / m };
|
|
34
|
+
}
|
|
35
|
+
/** Linear-light luminance-ish weight of an sRGB colour — handy for keeping companions balanced. */
|
|
36
|
+
export function relativeLuminance({ r, g, b }) {
|
|
37
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Largest moon radius (0.1 geometry × up to 0.22 scale). */
|
|
2
|
+
export declare const DEFAULT_MOON_RADIUS = 0.022;
|
|
3
|
+
/** How far a moon's orbit radius may stray from its nominal one, either way. */
|
|
4
|
+
export declare const MOON_ORBIT_HALF_SCATTER = 0.11;
|
|
5
|
+
/** Clear-space factor on (scene outer radius + system extent). */
|
|
6
|
+
export declare const STAR_DISTANCE_MARGIN = 2;
|
|
7
|
+
/** Largest angular radius the primary's disc may span seen from the planet, degrees. */
|
|
8
|
+
export declare const MAX_STAR_ANGULAR_RADIUS_DEG = 10;
|
|
9
|
+
export interface SceneSizes {
|
|
10
|
+
planetRadius: number;
|
|
11
|
+
/** Moon orbit radius (0 / omitted = no moons). */
|
|
12
|
+
moonOrbitRadius?: number;
|
|
13
|
+
moonRadius?: number;
|
|
14
|
+
/** Planet glow / cloud shell scale relative to the planet radius (e.g. 1.18). */
|
|
15
|
+
cloudOrGlowScale?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Radius of the sphere around the planet that holds the planet, its glow and its moons. */
|
|
18
|
+
export declare function sceneOuterRadius(s: SceneSizes): number;
|
|
19
|
+
export interface MinStarDistanceInput extends Partial<SceneSizes> {
|
|
20
|
+
/** Or pass the outer radius directly (wins over the sizes above). */
|
|
21
|
+
outerRadius?: number;
|
|
22
|
+
/** The star system's full extent from its barycentre (every disc, orbit and corona). */
|
|
23
|
+
systemExtent?: number;
|
|
24
|
+
/** The primary's radius, for the angular-size cap. */
|
|
25
|
+
primaryRadius?: number;
|
|
26
|
+
margin?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Minimum barycentre distance from the planet: `margin × (outer + systemExtent)`, and far enough
|
|
30
|
+
* that the primary spans at most `MAX_STAR_ANGULAR_RADIUS_DEG`. Pure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function minStarDistance(input: MinStarDistanceInput): number;
|
|
33
|
+
/** Sky margin: the system keeps this fraction of the sky radius clear of the skybox. */
|
|
34
|
+
export declare const SKY_MARGIN_FRACTION = 0.02;
|
|
35
|
+
/**
|
|
36
|
+
* Farthest barycentre distance that keeps a system of `systemExtent` inside a sky of `skyRadius`
|
|
37
|
+
* (with `SKY_MARGIN_FRACTION` of clearance). Never negative. Pure.
|
|
38
|
+
*/
|
|
39
|
+
export declare function maxStarDistance(skyRadius: number, systemExtent: number): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Scene bounds (pure): how far the star system must sit so it never touches the planet, its glow
|
|
2
|
+
// or its moons, and never looms over the planet. Hosts pass their scene sizes; hosts that pass
|
|
3
|
+
// nothing keep the author's distance as-is.
|
|
4
|
+
// The shared moons' sizes (`./moons` MOON_MAX_RADIUS and MOON_ORBIT_SCATTER / 2). Copied, not
|
|
5
|
+
// imported: this subpath is built to dist/ and the moons ship as source; a test pins them together.
|
|
6
|
+
/** Largest moon radius (0.1 geometry × up to 0.22 scale). */
|
|
7
|
+
export const DEFAULT_MOON_RADIUS = 0.022;
|
|
8
|
+
/** How far a moon's orbit radius may stray from its nominal one, either way. */
|
|
9
|
+
export const MOON_ORBIT_HALF_SCATTER = 0.11;
|
|
10
|
+
/** Clear-space factor on (scene outer radius + system extent). */
|
|
11
|
+
export const STAR_DISTANCE_MARGIN = 2;
|
|
12
|
+
/** Largest angular radius the primary's disc may span seen from the planet, degrees. */
|
|
13
|
+
export const MAX_STAR_ANGULAR_RADIUS_DEG = 10;
|
|
14
|
+
function pos(v, fallback) {
|
|
15
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : fallback;
|
|
16
|
+
}
|
|
17
|
+
/** Radius of the sphere around the planet that holds the planet, its glow and its moons. */
|
|
18
|
+
export function sceneOuterRadius(s) {
|
|
19
|
+
const planet = pos(s.planetRadius, 0) * Math.max(1, pos(s.cloudOrGlowScale, 1));
|
|
20
|
+
const orbit = pos(s.moonOrbitRadius, 0);
|
|
21
|
+
const moons = orbit > 0 ? orbit + MOON_ORBIT_HALF_SCATTER + pos(s.moonRadius, DEFAULT_MOON_RADIUS) : 0;
|
|
22
|
+
return Math.max(planet, moons);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimum barycentre distance from the planet: `margin × (outer + systemExtent)`, and far enough
|
|
26
|
+
* that the primary spans at most `MAX_STAR_ANGULAR_RADIUS_DEG`. Pure.
|
|
27
|
+
*/
|
|
28
|
+
export function minStarDistance(input) {
|
|
29
|
+
const outer = typeof input.outerRadius === 'number' && Number.isFinite(input.outerRadius)
|
|
30
|
+
? Math.max(0, input.outerRadius)
|
|
31
|
+
: sceneOuterRadius({ planetRadius: pos(input.planetRadius, 0), ...input });
|
|
32
|
+
const margin = typeof input.margin === 'number' && input.margin > 0 ? input.margin : STAR_DISTANCE_MARGIN;
|
|
33
|
+
const clear = margin * (outer + pos(input.systemExtent, 0));
|
|
34
|
+
const angular = pos(input.primaryRadius, 0) / Math.tan((MAX_STAR_ANGULAR_RADIUS_DEG * Math.PI) / 180);
|
|
35
|
+
return Math.max(clear, angular);
|
|
36
|
+
}
|
|
37
|
+
/** Sky margin: the system keeps this fraction of the sky radius clear of the skybox. */
|
|
38
|
+
export const SKY_MARGIN_FRACTION = 0.02;
|
|
39
|
+
/**
|
|
40
|
+
* Farthest barycentre distance that keeps a system of `systemExtent` inside a sky of `skyRadius`
|
|
41
|
+
* (with `SKY_MARGIN_FRACTION` of clearance). Never negative. Pure.
|
|
42
|
+
*/
|
|
43
|
+
export function maxStarDistance(skyRadius, systemExtent) {
|
|
44
|
+
const sky = typeof skyRadius === 'number' && Number.isFinite(skyRadius) ? Math.max(0, skyRadius) : Infinity;
|
|
45
|
+
const ext = typeof systemExtent === 'number' && Number.isFinite(systemExtent) ? Math.max(0, systemExtent) : 0;
|
|
46
|
+
return Math.max(0, sky * (1 - SKY_MARGIN_FRACTION) - ext);
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera distance (from the sphere's centre) at which a sphere of `radius` fits inside both the
|
|
3
|
+
* vertical and the horizontal field of view, with `margin` (1.15 = 15% breathing room).
|
|
4
|
+
*/
|
|
5
|
+
export declare function framingDistance(radius: number, fovDeg: number, aspect: number, margin?: number): number;
|
|
6
|
+
export interface StarPlacementInput {
|
|
7
|
+
/** Vertical field of view of the host camera, degrees. */
|
|
8
|
+
fovDeg: number;
|
|
9
|
+
/** Width / height of the viewport. */
|
|
10
|
+
aspect: number;
|
|
11
|
+
/** Distance of the host camera from the planet (the scene origin), on +Z looking at it. */
|
|
12
|
+
cameraDistance: number;
|
|
13
|
+
planetRadius: number;
|
|
14
|
+
/** Primary radius, scene units (default: `DEFAULT_STAR_PARAMS.radius`). */
|
|
15
|
+
starRadius?: number;
|
|
16
|
+
/** Star system distance from the origin (default: `DEFAULT_STAR_PARAMS.distance`). */
|
|
17
|
+
distance?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface StarPlacement {
|
|
20
|
+
azimuth: number;
|
|
21
|
+
elevation: number;
|
|
22
|
+
/** False when no in-frame spot clears the planet (it fills the frame); the star is then just above it. */
|
|
23
|
+
fits: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Azimuth / elevation that put the primary's disc (plus its bright glow) just outside the planet's
|
|
27
|
+
* angular disc and inside the frame of a camera on +Z looking at the origin — upper-right when there
|
|
28
|
+
* is room, swinging towards straight up as the frame narrows (portrait). Hosts use it only when the
|
|
29
|
+
* author has not set azimuth/elevation; `DEFAULT_STAR_PARAMS` stays the host-agnostic fallback.
|
|
30
|
+
*/
|
|
31
|
+
export declare function defaultStarPlacement(input: StarPlacementInput): StarPlacement;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Camera framing helpers (pure, no three): how far a camera must sit to fit a focus sphere, and a
|
|
2
|
+
// host-camera-aware default placement for the star so it reads beside the planet, not behind it.
|
|
3
|
+
import { FOCUS_GLOW_RADII } from './layout.js';
|
|
4
|
+
import { DEFAULT_STAR_PARAMS, STAR_PARAM_RANGES } from './params.js';
|
|
5
|
+
const DEG = Math.PI / 180;
|
|
6
|
+
/**
|
|
7
|
+
* Camera distance (from the sphere's centre) at which a sphere of `radius` fits inside both the
|
|
8
|
+
* vertical and the horizontal field of view, with `margin` (1.15 = 15% breathing room).
|
|
9
|
+
*/
|
|
10
|
+
export function framingDistance(radius, fovDeg, aspect, margin = 1.15) {
|
|
11
|
+
const r = Math.max(0, Number.isFinite(radius) ? radius : 0) * (Number.isFinite(margin) && margin > 0 ? margin : 1.15);
|
|
12
|
+
const halfV = (Math.min(179, Math.max(1, Number.isFinite(fovDeg) ? fovDeg : 45)) * DEG) / 2;
|
|
13
|
+
const a = Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
|
|
14
|
+
const halfH = Math.atan(Math.tan(halfV) * a);
|
|
15
|
+
return r / Math.sin(Math.min(halfV, halfH));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Azimuth / elevation that put the primary's disc (plus its bright glow) just outside the planet's
|
|
19
|
+
* angular disc and inside the frame of a camera on +Z looking at the origin — upper-right when there
|
|
20
|
+
* is room, swinging towards straight up as the frame narrows (portrait). Hosts use it only when the
|
|
21
|
+
* author has not set azimuth/elevation; `DEFAULT_STAR_PARAMS` stays the host-agnostic fallback.
|
|
22
|
+
*/
|
|
23
|
+
export function defaultStarPlacement(input) {
|
|
24
|
+
const fov = Number.isFinite(input.fovDeg) && input.fovDeg > 0 ? input.fovDeg : 45;
|
|
25
|
+
const aspect = Number.isFinite(input.aspect) && input.aspect > 0 ? input.aspect : 1;
|
|
26
|
+
const cd = Number.isFinite(input.cameraDistance) && input.cameraDistance > 0 ? input.cameraDistance : 4;
|
|
27
|
+
const pr = Math.max(0, Number.isFinite(input.planetRadius) ? input.planetRadius : 1);
|
|
28
|
+
const starRadius = input.starRadius ?? DEFAULT_STAR_PARAMS.radius;
|
|
29
|
+
const [dLo, dHi] = STAR_PARAM_RANGES.distance;
|
|
30
|
+
const D = Math.min(dHi, Math.max(dLo, input.distance ?? DEFAULT_STAR_PARAMS.distance));
|
|
31
|
+
// Work on the image plane at unit distance (tangent units): positions and radii both scale by tan.
|
|
32
|
+
const tanV = Math.tan((fov * DEG) / 2);
|
|
33
|
+
const tanH = tanV * aspect;
|
|
34
|
+
const planetTan = cd > pr ? pr / Math.sqrt(cd * cd - pr * pr) : Infinity;
|
|
35
|
+
const starTan = (starRadius * FOCUS_GLOW_RADII) / Math.sqrt(D * D + cd * cd);
|
|
36
|
+
const rho = planetTan + starTan * 1.35; // centre distance from the view axis: disc + glow clear the planet
|
|
37
|
+
const maxX = tanH * 0.97 - starTan * 0.5; // centre in frame, disc mostly in frame
|
|
38
|
+
const maxY = tanV * 0.97 - starTan * 0.5;
|
|
39
|
+
let x = 0;
|
|
40
|
+
let y = Number.isFinite(rho) ? rho : maxY;
|
|
41
|
+
let fits = false;
|
|
42
|
+
for (let phiDeg = 30; phiDeg <= 90; phiDeg += 2) {
|
|
43
|
+
const px = rho * Math.cos(phiDeg * DEG);
|
|
44
|
+
const py = rho * Math.sin(phiDeg * DEG);
|
|
45
|
+
if (px <= maxX && py <= maxY) {
|
|
46
|
+
x = px;
|
|
47
|
+
y = py;
|
|
48
|
+
fits = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Ray from the camera through (x, y) on the image plane, hit on the sphere |P| = D.
|
|
53
|
+
const len = Math.hypot(x, y, 1);
|
|
54
|
+
const ux = x / len;
|
|
55
|
+
const uy = y / len;
|
|
56
|
+
const uz = -1 / len;
|
|
57
|
+
const cu = cd * uz; // C · u with C = (0, 0, cd)
|
|
58
|
+
const t = -cu + Math.sqrt(Math.max(0, cu * cu - cd * cd + D * D));
|
|
59
|
+
const px = ux * t;
|
|
60
|
+
const py = uy * t;
|
|
61
|
+
const pz = cd + uz * t;
|
|
62
|
+
const round = (v) => Math.round(v * 10) / 10;
|
|
63
|
+
return {
|
|
64
|
+
azimuth: round(Math.atan2(px, -pz) / DEG),
|
|
65
|
+
elevation: round(Math.asin(Math.max(-1, Math.min(1, py / D))) / DEG),
|
|
66
|
+
fits,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { createStar } from './starBody.js';
|
|
2
|
+
export type { StarBody, StarFocus, CreateStarOptions } from './starBody.js';
|
|
3
|
+
export { StarManager } from './starManager.js';
|
|
4
|
+
export type { StarManagerOptions, StarSceneBounds } from './starManager.js';
|
|
5
|
+
export { STAR_QUALITY, normalizeStarQuality } from './quality.js';
|
|
6
|
+
export type { StarQuality, StarQualityProfile } from './quality.js';
|
|
7
|
+
export { DEFAULT_STAR_PARAMS, STAR_PARAM_RANGES, normalizeStarParams, spectralClass, } from './params.js';
|
|
8
|
+
export type { StarParams, StarMultiplicity, SpectralClass } from './params.js';
|
|
9
|
+
export { blackbodyColor } from './blackbody.js';
|
|
10
|
+
export type { RGB } from './blackbody.js';
|
|
11
|
+
export { starLayout, starPositions, starSystemPosition, starFocusRadius, flareAt } from './layout.js';
|
|
12
|
+
export { framingDistance, defaultStarPlacement } from './framing.js';
|
|
13
|
+
export type { StarPlacement, StarPlacementInput } from './framing.js';
|
|
14
|
+
export type { StarLayout, LaidOutStar, StarOrbit, StarRole, FlareState } from './layout.js';
|
|
15
|
+
export { starParamsFromStellarData, temperatureFromSpectralType, starCountFromPlanetName, luminosityClassFromSpectralType, mainSequenceLuminosity, mainSequenceRadius, mainSequenceTemperature, separationFromAu, youthFromAge, } from './stellarData.js';
|
|
16
|
+
export type { StellarData, StellarBinary, LuminosityClass } from './stellarData.js';
|
|
17
|
+
export { DEFAULT_AMBIENT, DEFAULT_STAR_LIGHTING, systemLightColor, keyLightIntensity, fillLightIntensity, lightingFromLegacy, LEGACY_AMBIENT_REFERENCE, LEGACY_AMBIENT_MAX, normalizeStarLighting, parseTint, } from './lighting.js';
|
|
18
|
+
export type { StarLighting, LegacyLighting, LegacyLightingResult } from './lighting.js';
|
|
19
|
+
export { minStarDistance, maxStarDistance, SKY_MARGIN_FRACTION, sceneOuterRadius, STAR_DISTANCE_MARGIN, MAX_STAR_ANGULAR_RADIUS_DEG, } from './bounds.js';
|
|
20
|
+
export type { SceneSizes, MinStarDistanceInput } from './bounds.js';
|
|
21
|
+
export { separateDirection, starsReach } from './layout.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// ./star — the host star of a world: an emissive, shader-glowing body (single, binary or trinary,
|
|
2
|
+
// plus an optional distant companion) in two draw calls, and — through StarManager — the scene's key
|
|
3
|
+
// light and camera-side fill. Pure param/layout/colour helpers are exported too so hosts can validate
|
|
4
|
+
// stored params and preview colours without WebGL.
|
|
5
|
+
export { createStar } from './starBody.js';
|
|
6
|
+
export { StarManager } from './starManager.js';
|
|
7
|
+
export { STAR_QUALITY, normalizeStarQuality } from './quality.js';
|
|
8
|
+
export { DEFAULT_STAR_PARAMS, STAR_PARAM_RANGES, normalizeStarParams, spectralClass, } from './params.js';
|
|
9
|
+
export { blackbodyColor } from './blackbody.js';
|
|
10
|
+
export { starLayout, starPositions, starSystemPosition, starFocusRadius, flareAt } from './layout.js';
|
|
11
|
+
export { framingDistance, defaultStarPlacement } from './framing.js';
|
|
12
|
+
export { starParamsFromStellarData, temperatureFromSpectralType, starCountFromPlanetName, luminosityClassFromSpectralType, mainSequenceLuminosity, mainSequenceRadius, mainSequenceTemperature, separationFromAu, youthFromAge, } from './stellarData.js';
|
|
13
|
+
export { DEFAULT_AMBIENT, DEFAULT_STAR_LIGHTING, systemLightColor, keyLightIntensity, fillLightIntensity, lightingFromLegacy, LEGACY_AMBIENT_REFERENCE, LEGACY_AMBIENT_MAX, normalizeStarLighting, parseTint, } from './lighting.js';
|
|
14
|
+
export { minStarDistance, maxStarDistance, SKY_MARGIN_FRACTION, sceneOuterRadius, STAR_DISTANCE_MARGIN, MAX_STAR_ANGULAR_RADIUS_DEG, } from './bounds.js';
|
|
15
|
+
export { separateDirection, starsReach } from './layout.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type RGB } from './blackbody.js';
|
|
2
|
+
import { type StarParams } from './params.js';
|
|
3
|
+
export type StarRole = 'primary' | 'companion' | 'wide' | 'distant';
|
|
4
|
+
export interface StarOrbit {
|
|
5
|
+
/** Distance from the barycentre it orbits, scene units. */
|
|
6
|
+
radius: number;
|
|
7
|
+
/** Phase at t = 0, radians. */
|
|
8
|
+
phase: number;
|
|
9
|
+
/** Radians per second (0 for a single star). */
|
|
10
|
+
angularSpeed: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LaidOutStar {
|
|
13
|
+
role: StarRole;
|
|
14
|
+
temperatureK: number;
|
|
15
|
+
color: RGB;
|
|
16
|
+
radius: number;
|
|
17
|
+
/** 0..1 corona intensity. */
|
|
18
|
+
luminosity: number;
|
|
19
|
+
/** 0..1 surface activity. */
|
|
20
|
+
activity: number;
|
|
21
|
+
/** Corona reach in radii (the billboard's half-size). */
|
|
22
|
+
coronaExtent: number;
|
|
23
|
+
/** Linear limb-darkening coefficient (cooler stars darken more towards the limb). */
|
|
24
|
+
limbDarkening: number;
|
|
25
|
+
/** 0..1 per-star noise offset so two stars never boil in sync. */
|
|
26
|
+
noiseSeed: number;
|
|
27
|
+
/** Photosphere brightness gain (1 for the system's stars; the distant companion is dimmer). */
|
|
28
|
+
gain: number;
|
|
29
|
+
orbit: StarOrbit;
|
|
30
|
+
}
|
|
31
|
+
export interface StarLayout {
|
|
32
|
+
stars: LaidOutStar[];
|
|
33
|
+
/** Tilt of the orbital plane from the local XZ plane, radians. */
|
|
34
|
+
inclination: number;
|
|
35
|
+
/** Rotation of the orbital plane about local Y, radians. */
|
|
36
|
+
node: number;
|
|
37
|
+
/** For trinaries: the close pair's barycentre orbit around the system barycentre. */
|
|
38
|
+
pairOrbit: StarOrbit | null;
|
|
39
|
+
/**
|
|
40
|
+
* The distant companion (S-type pair), or null: a lone star elsewhere in the sky at the system's
|
|
41
|
+
* distance, in the direction below (degrees). Not part of `stars`, orbits or the focus sphere.
|
|
42
|
+
*/
|
|
43
|
+
distant: {
|
|
44
|
+
star: LaidOutStar;
|
|
45
|
+
azimuth: number;
|
|
46
|
+
elevation: number;
|
|
47
|
+
} | null;
|
|
48
|
+
/** Radius of a sphere (around the local origin) that contains every star and its corona. */
|
|
49
|
+
boundingRadius: number;
|
|
50
|
+
/**
|
|
51
|
+
* Camera-focus radius: a sphere around the barycentre enclosing every star's orbit + disc and the
|
|
52
|
+
* bright inner glow (`FOCUS_GLOW_RADII` radii), not the faint outer corona — frames the system.
|
|
53
|
+
*/
|
|
54
|
+
focusRadius: number;
|
|
55
|
+
}
|
|
56
|
+
/** How much of the glow (in star radii) the focus sphere keeps around each disc. */
|
|
57
|
+
export declare const FOCUS_GLOW_RADII = 1.5;
|
|
58
|
+
/** Focus radius of a layout (pure): max over stars of (distance from barycentre + glow radius). */
|
|
59
|
+
export declare function starFocusRadius(layout: Pick<StarLayout, 'stars' | 'pairOrbit'>): number;
|
|
60
|
+
/** 32-bit string hash (the same one `generateMoons` uses). */
|
|
61
|
+
export declare function hashSeed(seed: number | string): number;
|
|
62
|
+
/** Linear congruential generator in [0, 1) (Numerical Recipes constants, as in the moons). */
|
|
63
|
+
export declare function createLcg(seed: number | string): () => number;
|
|
64
|
+
/** Linear limb-darkening coefficient vs temperature: ~0.85 for M dwarfs, ~0.6 Sun, ~0.3 O stars. */
|
|
65
|
+
export declare function limbDarkeningFor(temperatureK: number): number;
|
|
66
|
+
/** Corona half-size in radii for a luminosity 0..1. */
|
|
67
|
+
export declare function coronaExtentFor(luminosity: number): number;
|
|
68
|
+
/**
|
|
69
|
+
* Debris-disc orientation (pure, from the seed): the ring's plane is near-horizontal, tilted
|
|
70
|
+
* `tilt` radians towards a viewer on +Z (so it reads as a thin ellipse, ~0.2–0.45 axis ratio) and
|
|
71
|
+
* rolled `roll` radians about the view axis. Independent of the orbital plane so a face-on-ish
|
|
72
|
+
* binary orbit never turns the disc into a flat halo.
|
|
73
|
+
*/
|
|
74
|
+
export declare function discOrientation(seed: number | string): {
|
|
75
|
+
tilt: number;
|
|
76
|
+
roll: number;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Keep the distant companion's direction at least `minAngle` (radians) away from the system's,
|
|
80
|
+
* rotating it away along the great circle (or straight up when they coincide). Pure.
|
|
81
|
+
*/
|
|
82
|
+
export declare function separateDirection(sys: {
|
|
83
|
+
azimuth: number;
|
|
84
|
+
elevation: number;
|
|
85
|
+
}, far: {
|
|
86
|
+
azimuth: number;
|
|
87
|
+
elevation: number;
|
|
88
|
+
}, minAngle: number): {
|
|
89
|
+
azimuth: number;
|
|
90
|
+
elevation: number;
|
|
91
|
+
};
|
|
92
|
+
/** Farthest any system star's disc reaches from the barycentre (orbits + radius; no glow). */
|
|
93
|
+
export declare function starsReach(layout: Pick<StarLayout, 'stars' | 'pairOrbit'>): number;
|
|
94
|
+
/**
|
|
95
|
+
* Lay out the system for (possibly raw) params. Companions are always cooler and smaller than the
|
|
96
|
+
* primary and derived from it + the seed; the trinary's third is a small, cool, wide companion
|
|
97
|
+
* (an Alpha Centauri-like hierarchy). Deterministic: same params → same layout.
|
|
98
|
+
*/
|
|
99
|
+
export declare function starLayout(rawParams: Partial<StarParams> | unknown): StarLayout;
|
|
100
|
+
/**
|
|
101
|
+
* Local positions (relative to the system barycentre) of every star at `timeSec`, written as
|
|
102
|
+
* xyz triplets into `out` (allocated when omitted). Orbits are circular, in a plane tilted by
|
|
103
|
+
* `inclination` about X and rotated by `node` about Y. Allocation-free when `out` is passed.
|
|
104
|
+
*/
|
|
105
|
+
export declare function starPositions(layout: StarLayout, timeSec: number, out?: Float32Array | number[]): Float32Array | number[];
|
|
106
|
+
/** World-space position of the system barycentre for a distance/azimuth/elevation placement. */
|
|
107
|
+
export declare function starSystemPosition(p: Pick<StarParams, 'distance' | 'azimuth' | 'elevation'>): [number, number, number];
|
|
108
|
+
/** Length of one flare slot; at most one flare starts per slot. */
|
|
109
|
+
export declare const FLARE_SLOT_SEC = 6;
|
|
110
|
+
export interface FlareState {
|
|
111
|
+
/** 0..1 flare brightness right now. */
|
|
112
|
+
intensity: number;
|
|
113
|
+
/** Position angle of the flare on the limb, radians. */
|
|
114
|
+
angle: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Deterministic flare envelope for a star at `timeSec`: fast rise, exponential decay. The chance a
|
|
118
|
+
* slot holds a flare grows with `activity` (0 → never). Same (seed, time) → same answer on every
|
|
119
|
+
* machine, so all views of one world flare together.
|
|
120
|
+
*/
|
|
121
|
+
export declare function flareAt(noiseSeed: number, activity: number, timeSec: number, out?: FlareState): FlareState;
|