tonus 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BIBLIOGRAPHY.md CHANGED
@@ -126,6 +126,12 @@ doctrine tables, and the references behind them. Code cites by bracketed key
126
126
  shades) and III with the Canobic Inscription (c. 150).** The tetrachord
127
127
  genera for the tuning presets; the Greater Perfect System tone assignments
128
128
  and aspect–consonance mapping for the doctrina.
129
+ - `ptolemy-tetrabiblos` — **Ptolemy. _Tetrabiblos_ I.17 (the domiciles) and
130
+ I.19 (the exaltations) (c. 150).** The rulerships and the triplicity and
131
+ quadruplicity classifications, transmitted whole through the medieval Latin
132
+ tradition and uncontested across sources; the ground of the zodiac doctrine
133
+ table. The exaltation degrees it gives (the Sun at 19° Arietis and the rest)
134
+ are not carried — the sign is the resolution anything here reads.
129
135
  - `schulter-harmony` — **Schulter, Margo. Medieval Music & Arts Foundation
130
136
  harmony FAQs:** "Pythagorean Tuning and Medieval Polyphony"
131
137
  (<https://www.medieval.org/emfaq/harmony/pyth.html>) and "Hexachords,
package/CHANGELOG.md CHANGED
@@ -4,6 +4,72 @@ All notable changes to tonus. Newest first.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.6.0 — 2026-08-16
8
+
9
+ The zodiac learns what it means, and the hand stops sitting on its own floor.
10
+
11
+ ### Added
12
+
13
+ - **`ZODIACA` — the doctrine of the twelve signs.** `caelum` answers WHERE a
14
+ body is; this answers what a medieval reader brought to that position: the
15
+ element and its Galenic humor, the quality, the ruling and exalted planets,
16
+ and the member of the zodiac man the sign governs. `ZODIACA[body.zodiac]` is
17
+ the join, so a sky and its meaning are one lookup apart.
18
+
19
+ Deliberately no dates. When the Sun enters a sign is the ephemeris's business
20
+ and moves with precession — a table carrying "March 21" would be wrong for
21
+ most of the period this library models, and wrong differently every century.
22
+
23
+ The exaltation degrees are omitted (the sign is the resolution anything here
24
+ reads) and so are the lunar nodes, which are not tonus bodies. Five signs
25
+ exalt nobody, and that silence is the tradition's.
26
+
27
+ ### Changed — breaking
28
+
29
+ - **`SIGNS` and `SIGNA` are no longer exported.** `ZODIACA` carries each sign's
30
+ own `sign` and `signum` beside its doctrine, so it answers both what a sign
31
+ is called and what it means. The bare name arrays go back to being internal
32
+ to the engine that computes a longitude, which is the only place they were a
33
+ lookup rather than a listing.
34
+
35
+ ```js
36
+ // before
37
+ import tonus, { SIGNS, SIGNA } from "tonus";
38
+ SIGNS[body.zodiac]; // "Capricorn"
39
+ SIGNA[body.zodiac]; // "Capricornus"
40
+
41
+ // after
42
+ import tonus, { ZODIACA } from "tonus";
43
+ ZODIACA[body.zodiac].sign; // "Capricorn"
44
+ ZODIACA[body.zodiac].signum; // "Capricornus"
45
+ ```
46
+
47
+ ### Fixed
48
+
49
+ - **The Manus line reads the note that was clicked.** The subheader found its
50
+ row by pitch, and `find(r => r.spn === spn)` returns the FIRST note of the
51
+ chant at that pitch — so clicking the second E of a porrectus flexus
52
+ described an E in a torculus eleven notes earlier, with that note's neume,
53
+ joint and Guidonian name.
54
+
55
+ ### Site
56
+
57
+ - **The chant is lifted onto the hand automatically, by whole octaves.** Almost
58
+ nothing fits the gamut as printed: measured over 400 chants, 32 sit inside
59
+ their mode's gamut and 346 sit below it, so a piece read straight lands on
60
+ the bottom joint and stays there. The lift is computed from the chant's own
61
+ lowest note, which takes notes landing on a joint from 68.8% to 99.9%. The
62
+ `octava` button was the manual version of this and is gone.
63
+ - **The three sung hours become one Officium segment**, with all eight hours in
64
+ its popover — Matins and Vespers on, the little hours and Compline there to
65
+ be asked for.
66
+ - **The address bar keeps `cantus` and nothing else.** Eleven parameters were
67
+ mirrored there, each written the moment it moved off its default, which made
68
+ the bar a running transcript of a session.
69
+ - All three analysis tracks open together; the Similes subheader reports the
70
+ company a melody keeps rather than describing the list; the temperament
71
+ slider steps a tenth of a cent, the precision its readout prints.
72
+
7
73
  ## 0.5.0 — 2026-08-12
8
74
 
9
75
  Rendering, mostly — a day of looking at real chant on a real page and fixing
package/README.md CHANGED
@@ -1,79 +1,19 @@
1
1
  # tonus
2
2
 
3
3
  **tonus** is a JavaScript library for medieval music theory and chant
4
- analysis. It carries the Gregorian repertoire and the theory it was built on:
5
- it resolves what any day calls for, tunes chant in the ratios it was sung in,
6
- parses and draws the notation, voices the planets as pitch, and measures each
7
- chant in comparison.
4
+ analysis. It carries the Gregorian repertoire and the concepts it was built on.
5
+
6
+ It tunes to scales medieval singers would have used, tells you which chants
7
+ belong to which day and returns analysis and notation, and calculates the
8
+ _music of the spheres_ proposed by Pythagoras, Boethius, and more as real
9
+ intervals.
10
+
11
+ **[Interactive demo →](https://jeffreypierce.github.io/tonus/)**
8
12
 
9
13
  Everything is computed locally and deterministically: the same question always
10
14
  returns the same answer, from data that ships inside the package, with no network
11
15
  calls and no runtime dependencies.
12
16
 
13
- ## What it covers
14
-
15
- - **Pitch and tuning.** The medieval pitch world: Pythagorean and meantone
16
- temperaments, just intonation and custom scales, the Guidonian gamut and hand,
17
- hexachords and their mutations, and interval analysis by name.
18
- - **The eight modes.** Each church mode with its final, dominant, ambitus, and
19
- ethos, and the psalm tones that go with them.
20
- - **The liturgical calendar.** The Tridentine calendar of any year, resolved
21
- against Easter (by the Julian or the Gregorian computus), with each feast's
22
- rank, grade, and season.
23
- - **The chant repertoire.** About 2,200 chants across ten books (nine Solesmes
24
- editions and the Nocturnale Romanum night office), in
25
- [GABC](https://gregorio-project.github.io/) notation: Mass propers, the Kyriale,
26
- the Office hours, the psalter, and the Matins nocturns, retrievable by feast,
27
- season, mode, or office.
28
- - **Rhythm and notation.** GABC parsed into phrases, syllables, and tuned notes,
29
- shaped by the Solesmes arsis/thesis rhythm, with an analytic fingerprint of a
30
- chant's melodic character, and drawn to a square-note or modern staff.
31
- - **The harmony of the spheres.** An ephemeris of the classical planets, voiced
32
- as tuned pitch after Boethius, Nicomachus, Pliny, and Ptolemy.
33
- - **The census.** Every shipped chant measured against the corpus that holds
34
- it: how typical it is, where it is unusual, and its nearest neighbours by
35
- melodic shape.
36
-
37
- ## The ideas behind it
38
-
39
- Boethius's _De institutione musica_ carried Greek theory into the Latin
40
- Middle Ages, and it treats music as a branch of number, beside arithmetic,
41
- geometry, and astronomy. Its music runs from the audible to the inaudible:
42
- voices and instruments (_musica instrumentalis_), body and soul (_musica
43
- humana_), the turning cosmos (_musica mundana_). Chant was composed and
44
- copied inside that frame: pitch was ratio, the modes a received order, the
45
- heavens harmonic proportion.
46
-
47
- tonus takes those claims literally enough to compute them. It tunes pitch in
48
- the Pythagorean ratios of the treatises. It reads the calendar as the
49
- retrieval index it was for a millennium. It follows the Solesmes rhythm of
50
- arsis and thesis. And it computes _musica mundana_ as sounding pitch. Where
51
- the sources run out or contradict one another, tonus makes an editorial call
52
- and records it in the code.
53
-
54
- tonus is an instrument for study. It treats this repertoire as musical and
55
- historical material to be measured, queried, and understood; what it models is
56
- the sound and its structure.
57
-
58
- ## Sources
59
-
60
- tonus is built on primary and scholarly sources, and it names them wherever it
61
- makes a choice. The chant and calendar data are the Solesmes editions, by way of
62
- [GregoBase](https://gregobase.selapa.net/),
63
- [Divinum Officium](https://divinumofficium.com/), and the community _Nocturnale
64
- Romanum_. The rhythm follows Mocquereau, Gajard, Suñol, and Daniel Saulnier; the
65
- modes and cadences follow Rockstro's _Grove_ article, Niedermeyer & d'Ortigue,
66
- and Apel; the tuning follows Boethius's _De institutione musica_ and Ptolemy's
67
- _Harmonics_; the notation is engraved in Daniel Spreadbury's
68
- [Bravura](https://github.com/steinbergmedia/bravura), the reference font for the
69
- SMuFL standard; the planetary doctrines follow Godwin's sourcebooks of the
70
- Pythagorean tradition.
71
-
72
- Every figure is cited in the code beside what it explains. The complete list is
73
- [BIBLIOGRAPHY.md](BIBLIOGRAPHY.md).
74
-
75
- ## Example
76
-
77
17
  ```sh
78
18
  npm install tonus
79
19
  ```
@@ -92,8 +32,9 @@ const [feast] = tonus.festum({ date: new Date("2026-12-25") });
92
32
  const [introit] = tonus.proprium({ feast, office: "in" });
93
33
  // → "Puer natus est", mode 7, from the Liber Usualis
94
34
 
95
- // Turn the chant into tuned, rhythmicized notes.
35
+ // Turn the chant into tuned, rhythmicized notes, then draw it.
96
36
  const score = tonus.notatio(introit, { temperamentum: t });
37
+ const { svg, geometry } = tonus.inscriptio(score, { width: 680 });
97
38
 
98
39
  // And voice the Christmas sky as pitch, after Boethius.
99
40
  const harmony = tonus.harmonia(tonus.caelum({ date: feast.date }));
@@ -102,6 +43,56 @@ const harmony = tonus.harmonia(tonus.caelum({ date: feast.date }));
102
43
  The objects each call returns (`feast`, `t`) pass straight back into the next as
103
44
  filters.
104
45
 
46
+ ## The API
47
+
48
+ Fourteen methods, each named for what it returns.
49
+
50
+ | Method | Returns |
51
+ | ----------------------------- | ----------------------------------------------------------------- |
52
+ | `festum({ date })` | the feasts of a day, with rank, grade, and season |
53
+ | `pascha(year)` | Easter and the movable feasts reckoned from it |
54
+ | `cantus({ … })` | chants by id, incipit, mode, office, or source |
55
+ | `proprium({ feast, office })` | the Mass propers for a feast |
56
+ | `ordinarium({ … })` | the Kyriale — the Mass ordinary settings |
57
+ | `officium({ feast, hora })` | an Office hour, psalms and antiphons in order |
58
+ | `psalmus({ psalm })` | a psalm, pointed for its tone |
59
+ | `corpus()` | what ships: counts by book, genus, and mode |
60
+ | `census({ id })` | one chant measured against the corpus |
61
+ | `temperamentum({ … })` | a tuning — `nota`, `gradus`, `modus`, `intervallum` |
62
+ | `notatio(chant)` | GABC parsed to phrases, syllables, and tuned notes |
63
+ | `inscriptio(score)` | `{ svg, geometry }` — the score drawn, and where each note landed |
64
+ | `caelum({ date })` | the classical planets at a moment |
65
+ | `harmonia(caelum)` | those positions voiced as pitch |
66
+
67
+ **Tuning** covers Pythagorean and meantone temperaments, just intonation and
68
+ custom scales, the Guidonian gamut and hand, hexachords and their mutations,
69
+ and intervals named as the treatises name them. Each of the eight modes carries
70
+ its final, dominant, ambitus, and ethos, with its psalm tones.
71
+
72
+ **The calendar** is Tridentine, resolved against Easter by
73
+ the Julian or the Gregorian computus.
74
+
75
+ **The repertoire** is 2,187 chants across ten books — nine Solesmes editions and
76
+ the _Nocturnale Romanum_ night office — in
77
+ [GABC](https://gregorio-project.github.io/) notation: Mass propers, the Kyriale,
78
+ the Office hours, the psalter, and the Matins nocturns.
79
+
80
+ **Scores** are shaped by the Solesmes arsis/thesis rhythm, fingerprinted for
81
+ melodic character, and drawn to a square-note or modern staff.
82
+
83
+ ## Concepts
84
+
85
+ Boethius treated music as a branch of number theory, and divided it into the
86
+ audible and the inaudible: instruments and voices, body and soul, the turning
87
+ cosmos. Chant
88
+ was composed inside that frame: pitch was ratio, the modes a received order,
89
+ the heavens harmonic proportion. tonus takes those claims literally enough to
90
+ compute them, which is why tuning is ratio arithmetic, why the calendar is a
91
+ retrieval index, and why there is an ephemeris in a chant library at all.
92
+
93
+ Where the sources run out or contradict one another, tonus makes an editorial
94
+ call and records it in the code.
95
+
105
96
  ## Documentation
106
97
 
107
98
  The **[documentation index](docs/api/index.md)** is the technical center: the full
@@ -114,8 +105,7 @@ one page per engine, in dependency order:
114
105
  ## Install and run
115
106
 
116
107
  ESM only. Node ≥ 20; works in the browser through a bundler. No runtime
117
- dependencies. About 2,200 chants and the 650-entry calendar ship in the
118
- package.
108
+ dependencies. The chants and the 650-entry calendar ship in the package.
119
109
 
120
110
  ```sh
121
111
  npm test # builds and runs the suite (node --test)
@@ -124,6 +114,20 @@ npm test # builds and runs the suite (node --test)
124
114
  The data files in `src/data/` are generated by a separate extraction pipeline
125
115
  (tonus-corpus). Edits happen there, not here.
126
116
 
117
+ ## Sources
118
+
119
+ The chant and calendar data are the Solesmes editions, by way of
120
+ [GregoBase](https://gregobase.selapa.net/),
121
+ [Divinum Officium](https://divinumofficium.com/), and the community _Nocturnale
122
+ Romanum_. The notation is engraved in Daniel Spreadbury's
123
+ [Bravura](https://github.com/steinbergmedia/bravura), the reference font for the
124
+ SMuFL standard. Behind the rest stand Mocquereau and Suñol on rhythm, Rockstro
125
+ and Apel on the modes, Boethius and Ptolemy on tuning, and Godwin on the
126
+ Pythagorean planets.
127
+
128
+ Every figure is cited in the code beside what it explains, and
129
+ [BIBLIOGRAPHY.md](BIBLIOGRAPHY.md) is the complete list.
130
+
127
131
  ## License
128
132
 
129
133
  [PolyForm Noncommercial 1.0.0](LICENSE): free to use, study, modify, and
@@ -3,7 +3,7 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  // Codepoints and pitch positions mirror the gabc-smufl project's
5
5
  // data/gabc-map.json (SMuFL 1.5 / Bravura 1.392). Outlines for these codepoints
6
- // are baked into smufl-glyphs.json by scripts/extract-smufl-glyphs.py.
6
+ // are baked into smufl-glyphs.json by scripts/extract-smufl-glyphs.mjs.
7
7
  //
8
8
  // pitch_positions: the raw GABC pitch letter a–m IS the staff slot, in
9
9
  // half-staff-spaces from the bottom line, independent of clef. (a=-2 … c=0
@@ -1,17 +1,17 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // data/smufl-glyphs — SMuFL chant glyph outlines (baked from Bravura)
3
3
  // ---------------------------------------------------------------------------
4
- // Generated by scripts/extract-smufl-glyphs.py — do not edit smufl-glyphs.json
4
+ // Generated by `npm run glyphs` — do not edit smufl-glyphs.json
5
5
  // by hand. Paths are in font units, y-up (baseline at 0); the SVG emitter
6
6
  // applies scale(s, -s). See the script header for the coordinate contract.
7
7
 
8
8
  const data = ({
9
9
  "meta": {
10
10
  "source": "Bravura.otf (Steinberg Media, SIL OFL 1.1)",
11
- "smufl": "plainchant PUA (U+E8F0\u2013U+E9D9)",
11
+ "smufl": "plainchant PUA (U+E8F0–U+E9D9)",
12
12
  "upm": 1000,
13
13
  "coordinates": "font units, y-up (baseline at 0); emitter applies scale(s, -s)",
14
- "regenerate": "python3 scripts/extract-smufl-glyphs.py",
14
+ "regenerate": "npm run glyphs",
15
15
  "count": 138
16
16
  },
17
17
  "glyphs": {
@@ -1275,7 +1275,7 @@ const data = ({
1275
1275
  "advance": 215,
1276
1276
  "bbox": [
1277
1277
  0,
1278
- -173.08333333333334,
1278
+ -173.08333333333331,
1279
1279
  215,
1280
1280
  435
1281
1281
  ]
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "meta": {
3
3
  "source": "Bravura.otf (Steinberg Media, SIL OFL 1.1)",
4
- "smufl": "plainchant PUA (U+E8F0\u2013U+E9D9)",
4
+ "smufl": "plainchant PUA (U+E8F0–U+E9D9)",
5
5
  "upm": 1000,
6
6
  "coordinates": "font units, y-up (baseline at 0); emitter applies scale(s, -s)",
7
- "regenerate": "python3 scripts/extract-smufl-glyphs.py",
7
+ "regenerate": "npm run glyphs",
8
8
  "count": 138
9
9
  },
10
10
  "glyphs": {
@@ -1268,7 +1268,7 @@
1268
1268
  "advance": 215,
1269
1269
  "bbox": [
1270
1270
  0,
1271
- -173.08333333333334,
1271
+ -173.08333333333331,
1272
1272
  215,
1273
1273
  435
1274
1274
  ]
@@ -0,0 +1,47 @@
1
+ import type { BodyName } from "../../planet/types.js";
2
+ /** The four Aristotelian elements, as the triplicities assign them. */
3
+ export type Element = "fire" | "earth" | "air" | "water";
4
+ /** Ptolemy's quadruplicities, in the scholastic naming. */
5
+ export type Quality = "cardinal" | "fixed" | "mutable";
6
+ /** The Galenic humor the sign's element carries. */
7
+ export type Humor = "cholera" | "melancholia" | "sanguis" | "phlegma";
8
+ /** The part of the body a sign governs, in the zodiac man. */
9
+ export interface Melothesia {
10
+ /** The member as the manuscripts name it: caput, collum, pectus… */
11
+ latin: string;
12
+ /** What that covers, in English — several signs govern more than one part. */
13
+ english: string;
14
+ }
15
+ /** A sign's doctrine. English keys carry machine codes; the Latin fields carry
16
+ * the authentic terms a reader meets on the page. */
17
+ export interface ZodiacSign {
18
+ /** 0 = Aries … 11 = Pisces, matching `Body.zodiac`. */
19
+ index: number;
20
+ /** Matches `Body.sign` verbatim. */
21
+ sign: string;
22
+ /** Matches `Body.signum` verbatim — Scorpius, Capricornus. */
23
+ signum: string;
24
+ /** The astronomical symbol, U+2648–U+2653. */
25
+ symbol: string;
26
+ element: Element;
27
+ elementum: string;
28
+ quality: Quality;
29
+ qualitas: string;
30
+ /** The sign's nature as the two Aristotelian pairs, in Latin. */
31
+ nature: {
32
+ heat: string;
33
+ moisture: string;
34
+ };
35
+ genus: "masculinum" | "femininum";
36
+ humor: Humor;
37
+ temperament: string;
38
+ /** The planet that rules the sign. */
39
+ domicile: BodyName;
40
+ /** The planet exalted in it — null for the five that have none. */
41
+ exaltation: BodyName | null;
42
+ melothesia: Melothesia;
43
+ /** Where the sources disagree, said plainly rather than resolved. */
44
+ variant?: string;
45
+ }
46
+ export declare const ZODIACA: readonly ZodiacSign[];
47
+ //# sourceMappingURL=zodiac.d.ts.map
@@ -0,0 +1,163 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/harmonia/data/zodiac — what the twelve signs MEAN
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // `caelum` answers WHERE: a body's ecliptic longitude becomes `zodiac` (the
6
+ // index), `sign` and `signum`. This table answers WHAT IT MEANS — the doctrine
7
+ // a medieval reader brought to that position.
8
+ //
9
+ // DELIBERATELY NO DATES. When the Sun enters a sign is the ephemeris's
10
+ // business and changes with precession; a table that carried "March 21" would
11
+ // be wrong for most of the period this library models, and wrong differently
12
+ // every century. The index is the join, and `caelum` computes it.
13
+ //
14
+ // The join: `SIGNS[i]`, `SIGNA[i]` (engines/planet/planet.ts) and `ZODIACA[i]`
15
+ // are one list three ways, so `ZODIACA[body.zodiac]` is always that body's
16
+ // doctrine. The two name arrays live with the ephemeris because drawing the
17
+ // wheel needs them without any of this; the meanings live here because nothing
18
+ // in computing a position depends on them.
19
+ //
20
+ // Sources, per field:
21
+ // domicile Ptolemy, Tetrabiblos I.17 [biblio: ptolemy-tetrabiblos],
22
+ // transmitted whole through the medieval Latin tradition and
23
+ // uncontested across sources.
24
+ // exaltation Tetrabiblos I.19 and the standard medieval list, THE SEVEN
25
+ // PLANETS ONLY. The lunar nodes' exaltations (Caput Draconis in
26
+ // Gemini, Cauda in Sagittarius) are omitted because the nodes are
27
+ // not tonus bodies. The exaltation DEGREES (Sun at 19° Arietis
28
+ // and the rest) are omitted as more precision than any consumer
29
+ // here reads — five signs simply have no exaltation, and that
30
+ // absence is the tradition's, not an omission.
31
+ // element, the Ptolemaic triplicities and quadruplicities. The Latin
32
+ // quality quality terms are the scholastic mobile/fixum/commune, where
33
+ // Ptolemy writes tropic/solid/bicorporeal.
34
+ // humor the Galenic element→humor square — fire/cholera, earth/
35
+ // melancholia, air/sanguis, water/phlegma — ubiquitous in
36
+ // medieval medicine.
37
+ // melothesia the homo signorum of medieval calendars and medical
38
+ // manuscripts, Aries at the head down to Pisces at the feet (the
39
+ // Très Riches Heures anatomical man is the familiar picture).
40
+ // Practice, not decoration: phlebotomy was timed against it.
41
+ //
42
+ // Where the tradition wobbles, the entry says so in `variant` rather than
43
+ // picking a winner quietly. Uncertainty is data.
44
+ export const ZODIACA = [
45
+ {
46
+ index: 0, sign: "Aries", signum: "Aries", symbol: "♈",
47
+ element: "fire", elementum: "ignis",
48
+ quality: "cardinal", qualitas: "mobile",
49
+ nature: { heat: "calidum", moisture: "siccum" },
50
+ genus: "masculinum", humor: "cholera", temperament: "cholericum",
51
+ domicile: "Mars", exaltation: "Sun",
52
+ melothesia: { latin: "caput", english: "the head and face" },
53
+ },
54
+ {
55
+ index: 1, sign: "Taurus", signum: "Taurus", symbol: "♉",
56
+ element: "earth", elementum: "terra",
57
+ quality: "fixed", qualitas: "fixum",
58
+ nature: { heat: "frigidum", moisture: "siccum" },
59
+ genus: "femininum", humor: "melancholia", temperament: "melancholicum",
60
+ domicile: "Venus", exaltation: "Moon",
61
+ melothesia: { latin: "collum", english: "the neck and throat" },
62
+ },
63
+ {
64
+ index: 2, sign: "Gemini", signum: "Gemini", symbol: "♊",
65
+ element: "air", elementum: "aer",
66
+ quality: "mutable", qualitas: "commune",
67
+ nature: { heat: "calidum", moisture: "humidum" },
68
+ genus: "masculinum", humor: "sanguis", temperament: "sanguineum",
69
+ domicile: "Mercury", exaltation: null,
70
+ melothesia: { latin: "umeri", english: "the shoulders, arms and hands" },
71
+ },
72
+ {
73
+ index: 3, sign: "Cancer", signum: "Cancer", symbol: "♋",
74
+ element: "water", elementum: "aqua",
75
+ quality: "cardinal", qualitas: "mobile",
76
+ nature: { heat: "frigidum", moisture: "humidum" },
77
+ genus: "femininum", humor: "phlegma", temperament: "phlegmaticum",
78
+ domicile: "Moon", exaltation: "Jupiter",
79
+ melothesia: { latin: "pectus", english: "the breast and chest" },
80
+ variant: "the melothesia wobbles here more than anywhere: some manuscripts "
81
+ + "extend Cancer to the lungs and stomach",
82
+ },
83
+ {
84
+ index: 4, sign: "Leo", signum: "Leo", symbol: "♌",
85
+ element: "fire", elementum: "ignis",
86
+ quality: "fixed", qualitas: "fixum",
87
+ nature: { heat: "calidum", moisture: "siccum" },
88
+ genus: "masculinum", humor: "cholera", temperament: "cholericum",
89
+ domicile: "Sun", exaltation: null,
90
+ melothesia: { latin: "cor", english: "the heart, back and sides" },
91
+ },
92
+ {
93
+ index: 5, sign: "Virgo", signum: "Virgo", symbol: "♍",
94
+ element: "earth", elementum: "terra",
95
+ quality: "mutable", qualitas: "commune",
96
+ nature: { heat: "frigidum", moisture: "siccum" },
97
+ genus: "femininum", humor: "melancholia", temperament: "melancholicum",
98
+ domicile: "Mercury", exaltation: "Mercury",
99
+ melothesia: { latin: "venter", english: "the belly and bowels" },
100
+ },
101
+ {
102
+ index: 6, sign: "Libra", signum: "Libra", symbol: "♎",
103
+ element: "air", elementum: "aer",
104
+ quality: "cardinal", qualitas: "mobile",
105
+ nature: { heat: "calidum", moisture: "humidum" },
106
+ genus: "masculinum", humor: "sanguis", temperament: "sanguineum",
107
+ domicile: "Venus", exaltation: "Saturn",
108
+ melothesia: { latin: "renes", english: "the kidneys and loins" },
109
+ },
110
+ {
111
+ index: 7, sign: "Scorpio", signum: "Scorpius", symbol: "♏",
112
+ element: "water", elementum: "aqua",
113
+ quality: "fixed", qualitas: "fixum",
114
+ nature: { heat: "frigidum", moisture: "humidum" },
115
+ genus: "femininum", humor: "phlegma", temperament: "phlegmaticum",
116
+ domicile: "Mars", exaltation: null,
117
+ melothesia: { latin: "genitalia", english: "the genitals" },
118
+ },
119
+ {
120
+ index: 8, sign: "Sagittarius", signum: "Sagittarius", symbol: "♐",
121
+ element: "fire", elementum: "ignis",
122
+ quality: "mutable", qualitas: "commune",
123
+ nature: { heat: "calidum", moisture: "siccum" },
124
+ genus: "masculinum", humor: "cholera", temperament: "cholericum",
125
+ domicile: "Jupiter", exaltation: null,
126
+ melothesia: { latin: "femora", english: "the thighs" },
127
+ },
128
+ {
129
+ index: 9, sign: "Capricorn", signum: "Capricornus", symbol: "♑",
130
+ element: "earth", elementum: "terra",
131
+ quality: "cardinal", qualitas: "mobile",
132
+ nature: { heat: "frigidum", moisture: "siccum" },
133
+ genus: "femininum", humor: "melancholia", temperament: "melancholicum",
134
+ domicile: "Saturn", exaltation: "Mars",
135
+ melothesia: { latin: "genua", english: "the knees" },
136
+ },
137
+ {
138
+ index: 10, sign: "Aquarius", signum: "Aquarius", symbol: "♒",
139
+ element: "air", elementum: "aer",
140
+ quality: "fixed", qualitas: "fixum",
141
+ nature: { heat: "calidum", moisture: "humidum" },
142
+ genus: "masculinum", humor: "sanguis", temperament: "sanguineum",
143
+ domicile: "Saturn", exaltation: null,
144
+ melothesia: { latin: "crura", english: "the shins and ankles" },
145
+ },
146
+ {
147
+ index: 11, sign: "Pisces", signum: "Pisces", symbol: "♓",
148
+ element: "water", elementum: "aqua",
149
+ quality: "mutable", qualitas: "commune",
150
+ nature: { heat: "frigidum", moisture: "humidum" },
151
+ genus: "femininum", humor: "phlegma", temperament: "phlegmaticum",
152
+ domicile: "Jupiter", exaltation: "Venus",
153
+ melothesia: { latin: "pedes", english: "the feet" },
154
+ },
155
+ ];
156
+ // THE LUNAR PROHIBITION, recorded here rather than exported: while the Moon
157
+ // stands in a sign, that sign's member is not to be touched. It is the reason
158
+ // the zodiac man is in a physician's calendar at all — it was "dangerous, if
159
+ // not fatal, to treat that member if the Moon was in the sign at the time" —
160
+ // and it is one line of doctrine that any consumer can state for itself from
161
+ // `ZODIACA[moon.zodiac].melothesia`. A constant holding a sentence of English
162
+ // would be prose pretending to be data.
163
+ //# sourceMappingURL=zodiac.js.map
@@ -135,7 +135,14 @@ function resolveOpts(o) {
135
135
  rubricaColor: cssVar("rubrica", o.rubricaColor ?? "#9E2B25"),
136
136
  };
137
137
  }
138
- const HOUSE_SERIF = "'Crimson Pro', 'Crimson Text', 'EB Garamond', Garamond, Georgia, serif";
138
+ // JUNICODE FIRST, then the Garamonds. tonus ships no font bytes, so this stack
139
+ // is a request and not a guarantee — but naming the recommended face at its
140
+ // head costs nothing and means a page that already loads Junicode (the labs,
141
+ // the site, a medievalist's own stylesheet) gets the dress the plates are drawn
142
+ // with without passing a `fonts` option at all. Every name after it is the
143
+ // fallback that was here before, so a page without Junicode renders exactly as
144
+ // it did. See score.md → "The recommended face".
145
+ const HOUSE_SERIF = "Junicode, 'Crimson Pro', 'Crimson Text', 'EB Garamond', Garamond, Georgia, serif";
139
146
  // The books abbreviate the genus in the margin mark (Intr., Grad., Offert.);
140
147
  // a genus not in the table prints as-is with its period.
141
148
  const GENUS_ABBREV = {
package/dist/index.d.ts CHANGED
@@ -56,7 +56,7 @@ export { TONES } from "./engines/temper/data/tones.js";
56
56
  export type { PsalmTone, Differentia } from "./engines/temper/data/tones.js";
57
57
  export { CADENTIAE, CADENTIAE_POPULATION } from "./data/cadentiae.js";
58
58
  export type { CadentiaFamilia } from "./data/cadentiae.js";
59
- export { SIGNS, SIGNA } from "./engines/planet/planet.js";
59
+ export { ZODIACA } from "./engines/harmonia/data/zodiac.js";
60
60
  export { CENSUS_GROUPS, CENSUS_ORDER } from "./data/census.js";
61
61
  export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery, Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed, ChantTabulaRow, Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
62
62
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -54,8 +54,11 @@ export { SOURCES } from "./engines/chant/chant.js"; // book code → bibliograph
54
54
  export { MODES } from "./engines/temper/data/modes.js";
55
55
  export { TONES } from "./engines/temper/data/tones.js";
56
56
  export { CADENTIAE, CADENTIAE_POPULATION } from "./data/cadentiae.js";
57
- // planet — the zodiac
58
- export { SIGNS, SIGNA } from "./engines/planet/planet.js";
57
+ // planet — the zodiac. ZODIACA is the whole table: it carries each sign's own
58
+ // `sign` and `signum` beside its doctrine, so `ZODIACA[body.zodiac]` answers
59
+ // both what the sign is CALLED and what it MEANS. The bare name arrays stay
60
+ // inside the engine that computes a longitude, where they are the lookup.
61
+ export { ZODIACA } from "./engines/harmonia/data/zodiac.js";
59
62
  // census — the field groups and the block index. CENSUS_GROUPS keys are the
60
63
  // valid `by:` values AND the `profile` keys; CENSUS_ORDER holds every censused
61
64
  // id, so asking whether a chant is in the census stops needing a try/catch.
@@ -9,6 +9,7 @@ summarized in [Theory & Context](#theory--context).
9
9
  - [Heavens](#heavens)
10
10
  - [The heavens — `caelum`](#the-heavens--caelum)
11
11
  - [Aspects](#aspects)
12
+ - [The signs — `ZODIACA`](#the-signs--zodiaca)
12
13
  - [The voiced heavens — `harmonia`](#the-voiced-heavens--harmonia)
13
14
  - [The planetary vowels](#the-planetary-vowels)
14
15
  - [The tabula](#the-tabula)
@@ -129,6 +130,55 @@ interface Aspect {
129
130
  }
130
131
  ```
131
132
 
133
+ ## The signs — `ZODIACA`
134
+
135
+ `caelum` answers WHERE a body is. `ZODIACA` answers what that position MEANT:
136
+ the doctrine a medieval reader brought to a planet standing in a sign.
137
+
138
+ ```js
139
+ import tonus, { ZODIACA } from "tonus";
140
+
141
+ const [sun] = tonus.caelum({ date: new Date("2026-12-25") }).bodies;
142
+ const sign = ZODIACA[sun.zodiac];
143
+ // → { sign: "Capricorn", signum: "Capricornus", symbol: "♑",
144
+ // element: "earth", elementum: "terra", humor: "melancholia",
145
+ // quality: "cardinal", domicile: "Saturn", exaltation: "Mars",
146
+ // melothesia: { latin: "genua", english: "the knees" }, … }
147
+ ```
148
+
149
+ `ZODIACA[body.zodiac]` is the join. The array is in ecliptic order and each
150
+ entry knows its own `index`, so a body's sign is addressed directly rather than
151
+ searched for.
152
+
153
+ | Field | What it holds |
154
+ | ----------------------- | -------------------------------------------------------------------- |
155
+ | `sign` / `signum` | the code and the Latin — identical to `Body.sign` / `Body.signum` |
156
+ | `symbol` | the astronomical sign, U+2648–U+2653 |
157
+ | `element` / `elementum` | one of the four, and its Latin |
158
+ | `quality` / `qualitas` | cardinal, fixed, mutable — Ptolemy's tropic/solid/bicorporeal |
159
+ | `nature` | the two Aristotelian pairs: `calidum`/`frigidum`, `siccum`/`humidum` |
160
+ | `humor` / `temperament` | the Galenic humor the element carries, and its temperament |
161
+ | `domicile` | the planet that rules the sign, as a `BodyName` |
162
+ | `exaltation` | the planet exalted in it — `null` for the five that have none |
163
+ | `melothesia` | the member of the zodiac man the sign governs |
164
+ | `variant` | present only where the sources disagree |
165
+
166
+ **There are no dates.** When the Sun enters a sign is the ephemeris's business
167
+ and moves with precession; a table carrying "March 21" would be wrong for most
168
+ of the period this library models, and wrong differently every century.
169
+
170
+ **What is omitted, and why.** The exaltation degrees Ptolemy gives (the Sun at
171
+ 19° Arietis and the rest) are not carried — the sign is the resolution anything
172
+ here reads. Nor are the lunar nodes' exaltations, because the nodes are not
173
+ tonus bodies. Five signs exalt nobody: that silence is the tradition's, not a
174
+ gap in the table.
175
+
176
+ The `melothesia` is the *homo signorum* of medieval calendars — Aries at the
177
+ head down to Pisces at the feet. It was practice, not decoration: phlebotomy
178
+ was timed against it, and while the Moon stood in a sign its member was not to
179
+ be touched. Sourced from Ptolemy's *Tetrabiblos* I.17 and I.19
180
+ [biblio: ptolemy-tetrabiblos].
181
+
132
182
  ## The voiced heavens — `harmonia`
133
183
 
134
184
  `harmonia(cosmos, opts?)` gives each body a pitch and a Greek vowel according to
package/docs/api/index.md CHANGED
@@ -4,6 +4,8 @@ The technical center of tonus: the full public API, the conventions every method
4
4
  obeys, and the error contract. The API is **fourteen methods on the `tonus`
5
5
  namespace**, no sub-namespaces.
6
6
 
7
+ **[Interactive demo →](https://jeffreypierce.github.io/tonus/)**
8
+
7
9
  ```js
8
10
  import tonus from "tonus";
9
11
  ```
@@ -15,22 +17,22 @@ import tonus from "tonus";
15
17
 
16
18
  ## The methods
17
19
 
18
- | Method | Page | Returns |
19
- | ---------------------------------- | ----------------------- | ------------------------------------------------------- |
20
- | `tonus.festum(query?)` | [calendar](calendar.md) | `Feast[]` — liturgical calendar lookup |
21
- | `tonus.pascha(year)` | [calendar](calendar.md) | `Pascha` — the movable anchors of a liturgical year |
22
- | `tonus.cantus(query?)` | [chant](chant.md) | `Chant[]` — cross-corpus chant retrieval / GABC parsing |
23
- | `tonus.corpus(code?)` | [chant](chant.md) | `Corpus` — a book's ledger; no arg → the whole shelf |
24
- | `tonus.proprium(query?)` | [chant](chant.md) | `Chant[]` — Mass propers |
25
- | `tonus.ordinarium(query?)` | [chant](chant.md) | `OrdinaryChant[]` — Kyriale |
26
- | `tonus.officium(query?)` | [chant](chant.md) | `Chant[]` — Divine Office hours |
27
- | `tonus.psalmus(query?)` | [chant](chant.md) | `Chant[]` — intoned psalm verses |
28
- | `tonus.temperamentum(input?)` | [tuning](tuning.md) | `Temperamentum` — tuning context |
29
- | `tonus.notatio(chant, opts?)` | [score](score.md) | `Score` — GABC → musical score |
30
- | `tonus.inscriptio(score, opts?)` | [score](score.md) | `Inscriptio` — `{ svg, geometry }`, the score drawn |
31
- | `tonus.caelum(query?)` | [heavens](heavens.md) | `Cosmos \| Cosmos[]` — ephemeris |
32
- | `tonus.harmonia(cosmos, opts?)` | [heavens](heavens.md) | `Harmony` — musica universalis |
33
- | `tonus.census(query)` | [census](census.md) | `Census` — a chant against the corpus |
20
+ | Method | Page | Returns |
21
+ | -------------------------------- | ----------------------- | ------------------------------------------------------- |
22
+ | `tonus.festum(query?)` | [calendar](calendar.md) | `Feast[]` — liturgical calendar lookup |
23
+ | `tonus.pascha(year)` | [calendar](calendar.md) | `Pascha` — the movable anchors of a liturgical year |
24
+ | `tonus.cantus(query?)` | [chant](chant.md) | `Chant[]` — cross-corpus chant retrieval / GABC parsing |
25
+ | `tonus.corpus(code?)` | [chant](chant.md) | `Corpus` — a book's ledger; no arg → the whole shelf |
26
+ | `tonus.proprium(query?)` | [chant](chant.md) | `Chant[]` — Mass propers |
27
+ | `tonus.ordinarium(query?)` | [chant](chant.md) | `OrdinaryChant[]` — Kyriale |
28
+ | `tonus.officium(query?)` | [chant](chant.md) | `Chant[]` — Divine Office hours |
29
+ | `tonus.psalmus(query?)` | [chant](chant.md) | `Chant[]` — intoned psalm verses |
30
+ | `tonus.temperamentum(input?)` | [tuning](tuning.md) | `Temperamentum` — tuning context |
31
+ | `tonus.notatio(chant, opts?)` | [score](score.md) | `Score` — GABC → musical score |
32
+ | `tonus.inscriptio(score, opts?)` | [score](score.md) | `Inscriptio` — `{ svg, geometry }`, the score drawn |
33
+ | `tonus.caelum(query?)` | [heavens](heavens.md) | `Cosmos \| Cosmos[]` — ephemeris |
34
+ | `tonus.harmonia(cosmos, opts?)` | [heavens](heavens.md) | `Harmony` — musica universalis |
35
+ | `tonus.census(query)` | [census](census.md) | `Census` — a chant against the corpus |
34
36
 
35
37
  ### Query and builder functions
36
38
 
package/docs/api/score.md CHANGED
@@ -446,6 +446,39 @@ files: with `embed` it is a conduit for data the consumer supplies, so the
446
446
  consumer carries the face's license terms. Unset roles keep the house serif.
447
447
  `moderna` honours the `lyric`, `title`, and `annotation` slots.
448
448
 
449
+ #### The recommended face — Junicode
450
+
451
+ The plates are drawn in
452
+ **[Junicode](https://github.com/psb1558/Junicode-font)** (OFL), which the house
453
+ stack already asks for first. Load it and add the dress:
454
+
455
+ ```css
456
+ @font-face {
457
+ font-family: "Junicode";
458
+ src: url("JunicodeVF-Roman.woff2") format("woff2-variations");
459
+ font-weight: 300 700;
460
+ }
461
+ ```
462
+
463
+ ```js
464
+ tonus.inscriptio(score, {
465
+ width: 900,
466
+ theme: {
467
+ fonts: {
468
+ dropcap: { family: "Junicode", weight: 700 },
469
+ title: { family: "Junicode", weight: 620 },
470
+ annotation: { family: "Junicode", weight: 640 },
471
+ lyric: { family: "Junicode", weight: 560, scale: 1.06 },
472
+ },
473
+ },
474
+ dropcap: true, annotation: "auto", // a role shows only once its feature is on
475
+ });
476
+ ```
477
+
478
+ Reference it rather than `embed` it unless one SVG must travel alone: the face
479
+ is 196 KB base64'd, which triples a typical chant and repeats in every file,
480
+ where a reference is cached once.
481
+
449
482
  **`colors`** reach the SVG as CSS custom properties with the theme's own value
450
483
  as the fallback — `fill="var(--tonus-note, #111)"`. A rendered chant therefore
451
484
  carries the ink it was drawn with *and* stays themable: a host stylesheet that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonus",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
6
6
  "author": "Jeffrey Pierce <jeffrey@jeffreypierce.net>",
@@ -55,10 +55,12 @@
55
55
  "serve": "npm run site && node scripts/serve.mjs",
56
56
  "diagrams": "npm run site && node scripts/diagram-lab.mjs",
57
57
  "lab": "npm run build && node scripts/render-lab.mjs",
58
- "stress": "npm run build && node scripts/render-stress.mjs"
58
+ "stress": "npm run build && node scripts/render-stress.mjs",
59
+ "glyphs": "node scripts/extract-smufl-glyphs.mjs"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/node": "^22.0.0",
63
+ "fontkit": "^2.0.0",
62
64
  "typescript": "^5.6.3"
63
65
  },
64
66
  "engines": {