tonus 0.1.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.
Files changed (141) hide show
  1. package/BIBLIOGRAPHY.md +99 -0
  2. package/LICENSE +29 -0
  3. package/README.md +108 -0
  4. package/dist/data/cal.d.ts +10 -0
  5. package/dist/data/cal.js +3862 -0
  6. package/dist/data/commune.d.ts +17 -0
  7. package/dist/data/commune.js +1333 -0
  8. package/dist/data/gr.d.ts +5 -0
  9. package/dist/data/gr.js +13449 -0
  10. package/dist/data/kyriale.d.ts +11 -0
  11. package/dist/data/kyriale.js +971 -0
  12. package/dist/data/la.d.ts +5 -0
  13. package/dist/data/la.js +14229 -0
  14. package/dist/data/lh.d.ts +5 -0
  15. package/dist/data/lh.js +3619 -0
  16. package/dist/data/lu.d.ts +5 -0
  17. package/dist/data/lu.js +23779 -0
  18. package/dist/data/masses.d.ts +18 -0
  19. package/dist/data/masses.js +297 -0
  20. package/dist/data/office-roman.d.ts +19 -0
  21. package/dist/data/office-roman.js +13792 -0
  22. package/dist/data/office.d.ts +12 -0
  23. package/dist/data/office.js +13052 -0
  24. package/dist/data/propers.d.ts +13 -0
  25. package/dist/data/propers.js +7584 -0
  26. package/dist/data/psalms.d.ts +4 -0
  27. package/dist/data/psalms.js +10 -0
  28. package/dist/data/psalms.json +22918 -0
  29. package/dist/data/tones.d.ts +20 -0
  30. package/dist/data/tones.js +153 -0
  31. package/dist/data/types.d.ts +3 -0
  32. package/dist/data/types.js +2 -0
  33. package/dist/engines/cal/calendar.d.ts +21 -0
  34. package/dist/engines/cal/calendar.js +265 -0
  35. package/dist/engines/cal/date.d.ts +31 -0
  36. package/dist/engines/cal/date.js +141 -0
  37. package/dist/engines/cal/types.d.ts +66 -0
  38. package/dist/engines/cal/types.js +189 -0
  39. package/dist/engines/chant/chant.d.ts +10 -0
  40. package/dist/engines/chant/chant.js +135 -0
  41. package/dist/engines/chant/hour.d.ts +8 -0
  42. package/dist/engines/chant/hour.js +135 -0
  43. package/dist/engines/chant/intone.d.ts +8 -0
  44. package/dist/engines/chant/intone.js +84 -0
  45. package/dist/engines/chant/ordinary.d.ts +7 -0
  46. package/dist/engines/chant/ordinary.js +232 -0
  47. package/dist/engines/chant/propers.d.ts +8 -0
  48. package/dist/engines/chant/propers.js +107 -0
  49. package/dist/engines/chant/psalm.d.ts +7 -0
  50. package/dist/engines/chant/psalm.js +60 -0
  51. package/dist/engines/chant/syllabify.d.ts +20 -0
  52. package/dist/engines/chant/syllabify.js +192 -0
  53. package/dist/engines/chant/types.d.ts +76 -0
  54. package/dist/engines/chant/types.js +34 -0
  55. package/dist/engines/epoch.d.ts +2 -0
  56. package/dist/engines/epoch.js +14 -0
  57. package/dist/engines/harmonia/api.d.ts +35 -0
  58. package/dist/engines/harmonia/api.js +90 -0
  59. package/dist/engines/harmonia/aspects.d.ts +8 -0
  60. package/dist/engines/harmonia/aspects.js +15 -0
  61. package/dist/engines/harmonia/data/doctrines.d.ts +16 -0
  62. package/dist/engines/harmonia/data/doctrines.js +154 -0
  63. package/dist/engines/harmonia/data/vowels.d.ts +10 -0
  64. package/dist/engines/harmonia/data/vowels.js +21 -0
  65. package/dist/engines/harmonia/presence.d.ts +13 -0
  66. package/dist/engines/harmonia/presence.js +48 -0
  67. package/dist/engines/harmonia/tabula.d.ts +28 -0
  68. package/dist/engines/harmonia/tabula.js +32 -0
  69. package/dist/engines/harmonia/voice.d.ts +19 -0
  70. package/dist/engines/harmonia/voice.js +51 -0
  71. package/dist/engines/imprint.d.ts +30 -0
  72. package/dist/engines/imprint.js +152 -0
  73. package/dist/engines/planet/appearance.d.ts +40 -0
  74. package/dist/engines/planet/appearance.js +84 -0
  75. package/dist/engines/planet/aspects.d.ts +5 -0
  76. package/dist/engines/planet/aspects.js +41 -0
  77. package/dist/engines/planet/math.d.ts +13 -0
  78. package/dist/engines/planet/math.js +56 -0
  79. package/dist/engines/planet/orbital.d.ts +25 -0
  80. package/dist/engines/planet/orbital.js +223 -0
  81. package/dist/engines/planet/planet.d.ts +13 -0
  82. package/dist/engines/planet/planet.js +198 -0
  83. package/dist/engines/planet/position.d.ts +62 -0
  84. package/dist/engines/planet/position.js +156 -0
  85. package/dist/engines/planet/types.d.ts +61 -0
  86. package/dist/engines/planet/types.js +14 -0
  87. package/dist/engines/score/api.d.ts +54 -0
  88. package/dist/engines/score/api.js +87 -0
  89. package/dist/engines/score/articulation.d.ts +6 -0
  90. package/dist/engines/score/articulation.js +112 -0
  91. package/dist/engines/score/emitters/midi.d.ts +65 -0
  92. package/dist/engines/score/emitters/midi.js +158 -0
  93. package/dist/engines/score/emitters/musicxml.d.ts +18 -0
  94. package/dist/engines/score/emitters/musicxml.js +166 -0
  95. package/dist/engines/score/infer.d.ts +4 -0
  96. package/dist/engines/score/infer.js +77 -0
  97. package/dist/engines/score/ir.d.ts +4 -0
  98. package/dist/engines/score/ir.js +177 -0
  99. package/dist/engines/score/meta.d.ts +19 -0
  100. package/dist/engines/score/meta.js +34 -0
  101. package/dist/engines/score/neume.d.ts +3 -0
  102. package/dist/engines/score/neume.js +26 -0
  103. package/dist/engines/score/parse.d.ts +3 -0
  104. package/dist/engines/score/parse.js +359 -0
  105. package/dist/engines/score/phrasing.d.ts +24 -0
  106. package/dist/engines/score/phrasing.js +257 -0
  107. package/dist/engines/score/prosody.d.ts +35 -0
  108. package/dist/engines/score/prosody.js +109 -0
  109. package/dist/engines/score/tabula.d.ts +70 -0
  110. package/dist/engines/score/tabula.js +109 -0
  111. package/dist/engines/score/types.d.ts +159 -0
  112. package/dist/engines/score/types.js +2 -0
  113. package/dist/engines/temper/api.d.ts +60 -0
  114. package/dist/engines/temper/api.js +130 -0
  115. package/dist/engines/temper/data/constants.d.ts +27 -0
  116. package/dist/engines/temper/data/constants.js +150 -0
  117. package/dist/engines/temper/data/guido.d.ts +14 -0
  118. package/dist/engines/temper/data/guido.js +29 -0
  119. package/dist/engines/temper/data/modes.d.ts +38 -0
  120. package/dist/engines/temper/data/modes.js +158 -0
  121. package/dist/engines/temper/gabc.d.ts +5 -0
  122. package/dist/engines/temper/gabc.js +53 -0
  123. package/dist/engines/temper/gamut.d.ts +9 -0
  124. package/dist/engines/temper/gamut.js +24 -0
  125. package/dist/engines/temper/guido.d.ts +16 -0
  126. package/dist/engines/temper/guido.js +48 -0
  127. package/dist/engines/temper/interval.d.ts +15 -0
  128. package/dist/engines/temper/interval.js +31 -0
  129. package/dist/engines/temper/modes.d.ts +6 -0
  130. package/dist/engines/temper/modes.js +13 -0
  131. package/dist/engines/temper/neume.d.ts +14 -0
  132. package/dist/engines/temper/neume.js +59 -0
  133. package/dist/engines/temper/pitch.d.ts +40 -0
  134. package/dist/engines/temper/pitch.js +129 -0
  135. package/dist/engines/temper/scale.d.ts +37 -0
  136. package/dist/engines/temper/scale.js +217 -0
  137. package/dist/engines/temper/step.d.ts +23 -0
  138. package/dist/engines/temper/step.js +53 -0
  139. package/dist/index.d.ts +40 -0
  140. package/dist/index.js +27 -0
  141. package/package.json +60 -0
@@ -0,0 +1,51 @@
1
+ import { toPitch } from "../temper/pitch.js";
2
+ import { PLANET_VOWELS } from "./data/vowels.js";
3
+ import { computePresence, computeMotion } from "./presence.js";
4
+ // MIDI anchor for the doctrina's mese. A4 = MIDI 69 — matches the default temper
5
+ // reference. Ratios in the doctrina are multiplied by this anchor to produce Hz.
6
+ const ANCHOR_MIDI = 69;
7
+ function voiceOne(body, voice, vowel, scale) {
8
+ const pliny = voice.greekName === "proslambanomenos" && body.name === "Earth";
9
+ const presence = pliny ? 1 : computePresence(body);
10
+ const motion = pliny ? 0 : computeMotion(body);
11
+ // Direct Hz from anchor (A4) and doctrina ratio — bypass scale quantization
12
+ // so the ratio's pure Hz relationship is preserved.
13
+ const ratioVal = voice.ratio[0] / voice.ratio[1];
14
+ const hz = scale.a4 * ratioVal;
15
+ const midi = ANCHOR_MIDI + 12 * Math.log2(ratioVal);
16
+ // Nearest MIDI for Pitch metadata; override hz to the pure ratio-derived value.
17
+ const roundedMidi = Math.max(0, Math.min(127, Math.round(midi)));
18
+ const pitch = { ...toPitch(roundedMidi, scale), hz };
19
+ const performance = {
20
+ velocity: presence, // 0–1, same scale as the score engine
21
+ duration: 0,
22
+ rhythmicShape: "arsic",
23
+ rhythmicIndex: 1,
24
+ };
25
+ return {
26
+ ...body,
27
+ nota: { pitch, performance },
28
+ presence,
29
+ motion,
30
+ greekName: voice.greekName,
31
+ vowel,
32
+ };
33
+ }
34
+ export function voiceBodies(bodies, doctrina, scale) {
35
+ // Map doctrina body name → Voice entry
36
+ const voiceByBody = new Map();
37
+ for (const v of doctrina.voices)
38
+ voiceByBody.set(v.body, v);
39
+ const result = [];
40
+ for (const body of bodies) {
41
+ const voice = voiceByBody.get(body.name);
42
+ if (!voice)
43
+ continue; // body not part of this doctrina (e.g. Earth in Boethius)
44
+ const vowel = PLANET_VOWELS[body.name];
45
+ if (!vowel)
46
+ continue; // body has no classical vowel mapping (e.g. Earth, FixedStars)
47
+ result.push(voiceOne(body, voice, vowel, scale));
48
+ }
49
+ return result;
50
+ }
51
+ //# sourceMappingURL=voice.js.map
@@ -0,0 +1,30 @@
1
+ import type { Phrase } from "./score/types.js";
2
+ import type { Pitch } from "./temper/pitch.js";
3
+ import type { Scale } from "./temper/scale.js";
4
+ import type { VoicedBody } from "./harmonia/voice.js";
5
+ export interface Attractor {
6
+ pc: number;
7
+ weight: number;
8
+ pitch: Pitch;
9
+ }
10
+ export interface VowelAttractor {
11
+ vowel: string;
12
+ weight: number;
13
+ pitch: Pitch;
14
+ }
15
+ export interface ModalAffinity {
16
+ mode: number;
17
+ alias: string;
18
+ score: number;
19
+ }
20
+ export interface Imprint {
21
+ pcDistribution: Record<number, number>;
22
+ attractors: Attractor[];
23
+ vowelAttractors: VowelAttractor[];
24
+ modalAffinity: ModalAffinity[];
25
+ }
26
+ /** Build an Imprint from chant phrases (unweighted pc counts). */
27
+ export declare function computeImprint(phrases: Phrase[], scale: Scale): Imprint;
28
+ /** Build an Imprint from voiced planetary bodies (presence-weighted pc counts). */
29
+ export declare function computeImprintFromBodies(bodies: VoicedBody[], scale: Scale): Imprint;
30
+ //# sourceMappingURL=imprint.d.ts.map
@@ -0,0 +1,152 @@
1
+ import { toPitch } from "./temper/pitch.js";
2
+ import { MODES } from "./temper/modes.js";
3
+ const DEFAULT_TOP = 5;
4
+ const DEFAULT_MIDI_OCTAVE = 4;
5
+ const VOWELS = ["a", "e", "i", "o", "u"];
6
+ function pitchForPc(pc, scale) {
7
+ return toPitch(12 * (DEFAULT_MIDI_OCTAVE + 1) + pc, scale);
8
+ }
9
+ function computeAttractors(pcDistribution, scale, topN = DEFAULT_TOP) {
10
+ const entries = [];
11
+ for (let pc = 0; pc < 12; pc++) {
12
+ const w = pcDistribution[pc] ?? 0;
13
+ if (w > 0)
14
+ entries.push([pc, w]);
15
+ }
16
+ entries.sort((a, b) => b[1] - a[1]);
17
+ const top = entries.slice(0, topN);
18
+ const total = top.reduce((s, [, w]) => s + w, 0) || 1;
19
+ return top.map(([pc, w]) => ({
20
+ pc,
21
+ weight: w / total,
22
+ pitch: pitchForPc(pc, scale),
23
+ }));
24
+ }
25
+ function computeModalAffinity(pcDistribution) {
26
+ const results = [];
27
+ for (let m = 1; m <= 8; m++) {
28
+ const data = MODES.get(m);
29
+ if (!data)
30
+ continue;
31
+ const structural = new Set([
32
+ data.final,
33
+ data.tenor,
34
+ ...data.modulations.regular,
35
+ ]);
36
+ let score = 0;
37
+ for (const pc of structural)
38
+ score += pcDistribution[pc] ?? 0;
39
+ results.push({ mode: m, alias: data.alias, score });
40
+ }
41
+ return results.sort((a, b) => b.score - a.score);
42
+ }
43
+ function computeVowelAttractors(phrases, scale) {
44
+ const vowelPcMap = new Map();
45
+ for (const v of VOWELS)
46
+ vowelPcMap.set(v, new Map());
47
+ for (const phrase of phrases) {
48
+ for (const syl of phrase.syllables) {
49
+ for (const note of syl.notes) {
50
+ const vowel = note.context.vowel?.toLowerCase();
51
+ if (!vowel)
52
+ continue;
53
+ const pcMap = vowelPcMap.get(vowel);
54
+ if (!pcMap)
55
+ continue;
56
+ pcMap.set(note.pitch.pc, (pcMap.get(note.pitch.pc) ?? 0) + 1);
57
+ }
58
+ }
59
+ }
60
+ const vowelTotals = new Map();
61
+ for (const [v, pcMap] of vowelPcMap) {
62
+ let total = 0;
63
+ for (const w of pcMap.values())
64
+ total += w;
65
+ vowelTotals.set(v, total);
66
+ }
67
+ const grandTotal = Array.from(vowelTotals.values()).reduce((s, v) => s + v, 0);
68
+ if (grandTotal === 0)
69
+ return [];
70
+ const results = [];
71
+ for (const v of VOWELS) {
72
+ const pcMap = vowelPcMap.get(v);
73
+ if (pcMap.size === 0)
74
+ continue;
75
+ let bestPc = 0;
76
+ let bestW = -1;
77
+ for (const [pc, w] of pcMap) {
78
+ if (w > bestW) {
79
+ bestW = w;
80
+ bestPc = pc;
81
+ }
82
+ }
83
+ const total = vowelTotals.get(v) ?? 0;
84
+ results.push({ vowel: v, weight: total / grandTotal, pitch: pitchForPc(bestPc, scale) });
85
+ }
86
+ return results.sort((a, b) => b.weight - a.weight);
87
+ }
88
+ /** Build an Imprint from chant phrases (unweighted pc counts). */
89
+ export function computeImprint(phrases, scale) {
90
+ const pcCounts = new Array(12).fill(0);
91
+ let total = 0;
92
+ for (const phrase of phrases) {
93
+ for (const syl of phrase.syllables) {
94
+ for (const note of syl.notes) {
95
+ pcCounts[note.pitch.pc]++;
96
+ total++;
97
+ }
98
+ }
99
+ }
100
+ const pcDistribution = {};
101
+ for (let pc = 0; pc < 12; pc++) {
102
+ pcDistribution[pc] = total > 0 ? pcCounts[pc] / total : 0;
103
+ }
104
+ return {
105
+ pcDistribution,
106
+ attractors: computeAttractors(pcDistribution, scale),
107
+ vowelAttractors: computeVowelAttractors(phrases, scale),
108
+ modalAffinity: computeModalAffinity(pcDistribution),
109
+ };
110
+ }
111
+ /** Build an Imprint from voiced planetary bodies (presence-weighted pc counts). */
112
+ export function computeImprintFromBodies(bodies, scale) {
113
+ const pcWeight = new Array(12).fill(0);
114
+ let totalPresence = 0;
115
+ for (const b of bodies) {
116
+ pcWeight[b.nota.pitch.pc] += b.presence;
117
+ totalPresence += b.presence;
118
+ }
119
+ const pcDistribution = {};
120
+ for (let pc = 0; pc < 12; pc++) {
121
+ pcDistribution[pc] = totalPresence > 0 ? pcWeight[pc] / totalPresence : 0;
122
+ }
123
+ // Vowel attractors: harmony has no phrase text, but each voiced body carries
124
+ // its own Greek planetary vowel. Weight by presence; pitch is the body's own.
125
+ const vowelWeight = new Map();
126
+ let vowelTotal = 0;
127
+ for (const b of bodies) {
128
+ const key = b.vowel.phonetic;
129
+ const entry = vowelWeight.get(key);
130
+ if (entry) {
131
+ entry.weight += b.presence;
132
+ }
133
+ else {
134
+ vowelWeight.set(key, { weight: b.presence, pitch: b.nota.pitch });
135
+ }
136
+ vowelTotal += b.presence;
137
+ }
138
+ const vowelAttractors = [];
139
+ if (vowelTotal > 0) {
140
+ for (const [vowel, { weight, pitch }] of vowelWeight) {
141
+ vowelAttractors.push({ vowel, weight: weight / vowelTotal, pitch });
142
+ }
143
+ vowelAttractors.sort((a, b) => b.weight - a.weight);
144
+ }
145
+ return {
146
+ pcDistribution,
147
+ attractors: computeAttractors(pcDistribution, scale),
148
+ vowelAttractors,
149
+ modalAffinity: computeModalAffinity(pcDistribution),
150
+ };
151
+ }
152
+ //# sourceMappingURL=imprint.js.map
@@ -0,0 +1,40 @@
1
+ export interface SunAppearance {
2
+ elongation: 0;
3
+ phaseAngle: 0;
4
+ phase: 1;
5
+ magnitude: -26.74;
6
+ apparentDiameter: number;
7
+ }
8
+ export declare function sunAppearance(distAu: number): SunAppearance;
9
+ export interface MoonAppearance {
10
+ elongation: number;
11
+ phaseAngle: number;
12
+ phase: number;
13
+ apparentDiameter: number;
14
+ }
15
+ export declare function moonAppearance(config: {
16
+ sunLongitude: number;
17
+ moonLongitude: number;
18
+ moonLatitude: number;
19
+ distEarthRadii: number;
20
+ }): MoonAppearance;
21
+ export interface PlanetAppearance {
22
+ elongation: number;
23
+ phaseAngle: number;
24
+ phase: number;
25
+ apparentDiameter: number | {
26
+ equ: number;
27
+ pol: number;
28
+ };
29
+ magnitude: number;
30
+ }
31
+ export declare function planetAppearance(config: {
32
+ name: string;
33
+ heliocentricDistance: number;
34
+ geocentricDistance: number;
35
+ geocentricLongitude: number;
36
+ geocentricLatitude: number;
37
+ sunDistance: number;
38
+ J: number;
39
+ }): PlanetAppearance;
40
+ //# sourceMappingURL=appearance.d.ts.map
@@ -0,0 +1,84 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/planet/appearance — magnitude, phase, elongation, apparent diameter
3
+ // ---------------------------------------------------------------------------
4
+ import { sinDeg, cosDeg, asinDeg, acosDeg, wrapAngle } from "./math.js";
5
+ // Apparent diameter at 1 AU (arcsec) — flat for Mercury/Venus/Sun, obj for others
6
+ const DIAMETERS_AT_1AU = new Map([
7
+ ["Mercury", 6.74],
8
+ ["Venus", 16.92],
9
+ ["Earth", { equ: 17.59, pol: 17.53 }],
10
+ ["Mars", { equ: 9.36, pol: 9.28 }],
11
+ ["Jupiter", { equ: 196.94, pol: 185.08 }],
12
+ ["Saturn", { equ: 165.6, pol: 150.8 }],
13
+ ["Uranus", { equ: 65.8, pol: 62.1 }],
14
+ ["Neptune", { equ: 62.2, pol: 60.9 }],
15
+ ["Sun", 1919.26],
16
+ ]);
17
+ const apparentDiam = (d0, dist) => d0 / dist;
18
+ export function sunAppearance(distAu) {
19
+ return {
20
+ elongation: 0,
21
+ phaseAngle: 0,
22
+ phase: 1,
23
+ magnitude: -26.74,
24
+ apparentDiameter: apparentDiam(1919.26, distAu),
25
+ };
26
+ }
27
+ function moonElongation(sunLon, moonLon, moonLat) {
28
+ const delta = wrapAngle(sunLon - moonLon);
29
+ return acosDeg(cosDeg(delta) * cosDeg(moonLat));
30
+ }
31
+ export function moonAppearance(config) {
32
+ const { sunLongitude, moonLongitude, moonLatitude, distEarthRadii } = config;
33
+ const elong = moonElongation(sunLongitude, moonLongitude, moonLatitude);
34
+ const phaseAngle = 180 - elong;
35
+ const phase = (1 + cosDeg(phaseAngle)) / 2;
36
+ return {
37
+ elongation: elong,
38
+ phaseAngle,
39
+ phase,
40
+ apparentDiameter: (1873.7 * 60) / distEarthRadii,
41
+ };
42
+ }
43
+ function saturnRingTilt(los, las, J) {
44
+ const ir = 28.06;
45
+ const Nr = 169.51 + 3.82e-5 * J;
46
+ return asinDeg(sinDeg(las) * cosDeg(ir) - cosDeg(las) * sinDeg(ir) * sinDeg(los - Nr));
47
+ }
48
+ function planetMagnitude(name, r, R, FV, extras) {
49
+ const base = 5 * Math.log10(r * R);
50
+ switch (name) {
51
+ case "Mercury": return -0.36 + base + 0.027 * FV + 2.2e-13 * FV ** 6;
52
+ case "Venus": return -4.34 + base + 0.013 * FV + 4.2e-7 * FV ** 3;
53
+ case "Mars": return -1.51 + base + 0.016 * FV;
54
+ case "Jupiter": return -9.25 + base + 0.014 * FV;
55
+ case "Saturn": {
56
+ const B = saturnRingTilt(extras?.los ?? 0, extras?.las ?? 0, extras?.J ?? 0);
57
+ return -9.0 + base + 0.044 * FV + (-2.6 * Math.abs(sinDeg(B)) + 1.2 * sinDeg(B) ** 2);
58
+ }
59
+ case "Uranus": return -7.15 + base + 0.001 * FV;
60
+ case "Neptune": return -6.9 + base + 0.001 * FV;
61
+ default: return 0;
62
+ }
63
+ }
64
+ export function planetAppearance(config) {
65
+ const { name, heliocentricDistance: r, geocentricDistance: R, sunDistance: s, geocentricLongitude, geocentricLatitude, J } = config;
66
+ // elongation: angle at Earth between Sun and planet
67
+ const elongation = acosDeg((s * s + R * R - r * r) / (2 * s * R));
68
+ // phase angle: angle at planet between Sun and Earth
69
+ const phaseAngle = acosDeg((r * r + R * R - s * s) / (2 * r * R));
70
+ const phase = (1 + cosDeg(phaseAngle)) / 2;
71
+ const d0 = DIAMETERS_AT_1AU.get(name);
72
+ let apparentDiameter = 0;
73
+ if (typeof d0 === "number") {
74
+ apparentDiameter = apparentDiam(d0, R);
75
+ }
76
+ else if (d0) {
77
+ apparentDiameter = { equ: apparentDiam(d0.equ, R), pol: apparentDiam(d0.pol, R) };
78
+ }
79
+ const magnitude = planetMagnitude(name, r, R, phaseAngle, {
80
+ los: geocentricLongitude, las: geocentricLatitude, J,
81
+ });
82
+ return { elongation, phaseAngle, phase, apparentDiameter, magnitude };
83
+ }
84
+ //# sourceMappingURL=appearance.js.map
@@ -0,0 +1,5 @@
1
+ import type { Aspect } from "./types.js";
2
+ export declare function detectAspects(bodies: Record<string, number>, opts?: {
3
+ orbLimit?: number;
4
+ }): Aspect[];
5
+ //# sourceMappingURL=aspects.d.ts.map
@@ -0,0 +1,41 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/planet/aspects — angular aspect detection
3
+ // ---------------------------------------------------------------------------
4
+ import { wrapAngle } from "./math.js";
5
+ const ASPECTS = [
6
+ ["conjunction", 0],
7
+ ["sextile", 60],
8
+ ["square", 90],
9
+ ["trine", 120],
10
+ ["opposition", 180],
11
+ ];
12
+ const DEFAULT_ORB = 8;
13
+ function separation(a, b) {
14
+ const d = Math.abs(wrapAngle(b - a));
15
+ return d > 180 ? 360 - d : d;
16
+ }
17
+ export function detectAspects(bodies, opts) {
18
+ const orbLimit = opts?.orbLimit ?? DEFAULT_ORB;
19
+ const names = Object.keys(bodies);
20
+ const results = [];
21
+ for (let i = 0; i < names.length; i++) {
22
+ for (let j = i + 1; j < names.length; j++) {
23
+ const sep = separation(bodies[names[i]], bodies[names[j]]);
24
+ for (const [type, angle] of ASPECTS) {
25
+ const orb = Math.abs(sep - angle);
26
+ if (orb <= orbLimit) {
27
+ results.push({
28
+ type,
29
+ bodies: [names[i], names[j]],
30
+ angle: sep,
31
+ orb,
32
+ strength: Math.max(0, 1 - orb / orbLimit),
33
+ });
34
+ }
35
+ }
36
+ }
37
+ }
38
+ results.sort((a, b) => b.strength - a.strength || a.type.localeCompare(b.type));
39
+ return results;
40
+ }
41
+ //# sourceMappingURL=aspects.js.map
@@ -0,0 +1,13 @@
1
+ export declare const sinDeg: (d: number) => number;
2
+ export declare const cosDeg: (d: number) => number;
3
+ export declare const asinDeg: (v: number) => number;
4
+ export declare const acosDeg: (v: number) => number;
5
+ export declare const atan2Deg: (y: number, x: number) => number;
6
+ export declare function kepler(M: number, e: number, tol?: number): number;
7
+ export declare const wrapAngle: (deg: number) => number;
8
+ export declare function angleDelta(a: number, b: number): number;
9
+ export declare const toAu: (km: number) => number;
10
+ export declare const toCartesian: (lon: number, lat: number, dist: number) => [number, number, number];
11
+ export declare const toSpherical: (x: number, y: number, z: number) => [number, number, number];
12
+ export declare const toEquatorial: (x: number, y: number, z: number, eps: number) => [number, number, number];
13
+ //# sourceMappingURL=math.d.ts.map
@@ -0,0 +1,56 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/planet/math — pure trig/math helpers
3
+ // ---------------------------------------------------------------------------
4
+ const DEG2RAD = Math.PI / 180;
5
+ const RAD2DEG = 180 / Math.PI;
6
+ export const sinDeg = (d) => Math.sin(d * DEG2RAD);
7
+ export const cosDeg = (d) => Math.cos(d * DEG2RAD);
8
+ export const asinDeg = (v) => Math.asin(Math.max(-1, Math.min(1, v))) * RAD2DEG;
9
+ export const acosDeg = (v) => Math.acos(Math.max(-1, Math.min(1, v))) * RAD2DEG;
10
+ export const atan2Deg = (y, x) => Math.atan2(y, x) * RAD2DEG;
11
+ // Solve Kepler's equation M = E - e*sin(E) for E (eccentric anomaly), all in degrees
12
+ export function kepler(M, e, tol = 1e-7) {
13
+ let E = M + RAD2DEG * e * sinDeg(M);
14
+ for (let i = 0; i < 80; i++) {
15
+ const dM = E - e * RAD2DEG * sinDeg(E) - M;
16
+ const dE = -dM / (1 - e * cosDeg(E));
17
+ E += dE;
18
+ if (Math.abs(dE) < tol)
19
+ break;
20
+ }
21
+ return E;
22
+ }
23
+ // Wrap angle to 0–360
24
+ export const wrapAngle = (deg) => {
25
+ const a = deg % 360;
26
+ return a < 0 ? a + 360 : a;
27
+ };
28
+ // Signed angular difference (shortest arc, -180 to +180)
29
+ export function angleDelta(a, b) {
30
+ let d = wrapAngle(b - a);
31
+ if (d > 180)
32
+ d -= 360;
33
+ return d;
34
+ }
35
+ // km → AU
36
+ export const toAu = (km) => km * 6.6845871222684e-9;
37
+ // Ecliptic spherical → Cartesian (lon, lat in deg, dist in any unit)
38
+ export const toCartesian = (lon, lat, dist) => [
39
+ dist * cosDeg(lon) * cosDeg(lat),
40
+ dist * sinDeg(lon) * cosDeg(lat),
41
+ dist * sinDeg(lat),
42
+ ];
43
+ // Cartesian → ecliptic spherical [lon, lat, dist]
44
+ export const toSpherical = (x, y, z) => {
45
+ const lon = atan2Deg(y, x);
46
+ const lat = atan2Deg(z, Math.sqrt(x * x + y * y));
47
+ const dist = Math.sqrt(x * x + y * y + z * z);
48
+ return [lon, lat, dist];
49
+ };
50
+ // Rotate ecliptic Cartesian to equatorial by obliquity ε (deg)
51
+ export const toEquatorial = (x, y, z, eps) => [
52
+ x,
53
+ y * cosDeg(eps) - z * sinDeg(eps),
54
+ y * sinDeg(eps) + z * cosDeg(eps),
55
+ ];
56
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1,25 @@
1
+ export interface OrbitalElements {
2
+ name: string;
3
+ symbol: string;
4
+ datasets: [
5
+ [
6
+ number,
7
+ number
8
+ ][],
9
+ [
10
+ number,
11
+ number
12
+ ][],
13
+ [
14
+ number,
15
+ number,
16
+ number,
17
+ number
18
+ ]?
19
+ ];
20
+ radius: number;
21
+ rotation_period: number;
22
+ }
23
+ export type PerturbCoeffs = [number, number, number, number];
24
+ export declare const ORBITAL_ELEMENTS: Map<string, OrbitalElements>;
25
+ //# sourceMappingURL=orbital.d.ts.map