tonus 0.1.6 → 0.5.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 (161) hide show
  1. package/BIBLIOGRAPHY.md +132 -108
  2. package/CHANGELOG.md +610 -1
  3. package/LICENSE +133 -29
  4. package/README.md +106 -83
  5. package/dist/data/am.js +2669 -11196
  6. package/dist/data/ams.d.ts +5 -0
  7. package/dist/data/ams.js +122 -0
  8. package/dist/data/attestation.d.ts +19 -0
  9. package/dist/data/attestation.js +15716 -0
  10. package/dist/data/attestation.json +15711 -0
  11. package/dist/data/cadentiae.d.ts +43 -0
  12. package/dist/data/cadentiae.js +174 -0
  13. package/dist/data/cal.js +48 -0
  14. package/dist/data/census.d.ts +12 -0
  15. package/dist/data/census.js +36 -0
  16. package/dist/data/commune-office.d.ts +4 -0
  17. package/dist/data/commune-office.js +2371 -0
  18. package/dist/data/commune-office.json +2365 -0
  19. package/dist/data/commune.js +181 -11
  20. package/dist/data/corpus-overlap.d.ts +24 -0
  21. package/dist/data/corpus-overlap.js +347 -0
  22. package/dist/data/cot.d.ts +5 -0
  23. package/dist/data/cot.js +172 -0
  24. package/dist/data/cse.d.ts +5 -0
  25. package/dist/data/cse.js +122 -0
  26. package/dist/data/gabc-glyphs.d.ts +45 -0
  27. package/dist/data/gabc-glyphs.js +122 -0
  28. package/dist/data/gr.js +757 -6394
  29. package/dist/data/kyriale.js +116 -116
  30. package/dist/data/la.js +802 -13419
  31. package/dist/data/lh.js +116 -3473
  32. package/dist/data/lu.js +935 -17632
  33. package/dist/data/nocturnale-romanum.d.ts +5 -0
  34. package/dist/data/nocturnale-romanum.js +3772 -0
  35. package/dist/data/office-ferial.d.ts +4 -0
  36. package/dist/data/office-ferial.js +396 -0
  37. package/dist/data/office-ferial.json +391 -0
  38. package/dist/data/office-monastic.d.ts +17 -1
  39. package/dist/data/office-monastic.js +1403 -466
  40. package/dist/data/office-psalms-monastic.d.ts +13 -1
  41. package/dist/data/office-psalms-monastic.js +9 -0
  42. package/dist/data/propers.js +1 -1
  43. package/dist/data/psalms.js +22919 -5
  44. package/dist/data/psm.d.ts +5 -0
  45. package/dist/data/psm.js +122 -0
  46. package/dist/data/seasonal-respbreve.d.ts +5 -0
  47. package/dist/data/seasonal-respbreve.js +41 -0
  48. package/dist/data/seasonal-respbreve.json +35 -0
  49. package/dist/data/smufl-glyphs.d.ts +17 -0
  50. package/dist/data/smufl-glyphs.js +1546 -0
  51. package/dist/data/smufl-glyphs.json +1530 -0
  52. package/dist/engines/cal/calendar.d.ts +3 -2
  53. package/dist/engines/cal/calendar.js +105 -29
  54. package/dist/engines/cal/data/eras.d.ts +35 -0
  55. package/dist/engines/cal/data/eras.js +128 -0
  56. package/dist/engines/cal/date.js +44 -0
  57. package/dist/engines/cal/types.d.ts +15 -3
  58. package/dist/engines/cal/types.js +5 -5
  59. package/dist/engines/census/census.d.ts +7 -0
  60. package/dist/engines/census/census.js +179 -0
  61. package/dist/engines/census/types.d.ts +55 -0
  62. package/dist/engines/census/types.js +8 -0
  63. package/dist/engines/chant/attest.d.ts +39 -0
  64. package/dist/engines/chant/attest.js +90 -0
  65. package/dist/engines/chant/chant.d.ts +22 -4
  66. package/dist/engines/chant/chant.js +274 -14
  67. package/dist/engines/chant/data/compline.js +2 -1
  68. package/dist/engines/chant/data/masses.d.ts +56 -4
  69. package/dist/engines/chant/data/masses.js +305 -80
  70. package/dist/engines/chant/data/prime.js +1 -1
  71. package/dist/engines/chant/hour.js +279 -58
  72. package/dist/engines/chant/ordinary.d.ts +2 -0
  73. package/dist/engines/chant/ordinary.js +336 -56
  74. package/dist/engines/chant/propers.js +55 -5
  75. package/dist/engines/chant/psalm.d.ts +4 -4
  76. package/dist/engines/chant/psalm.js +25 -11
  77. package/dist/engines/chant/syllabify.d.ts +1 -0
  78. package/dist/engines/chant/syllabify.js +90 -17
  79. package/dist/engines/chant/types.d.ts +145 -10
  80. package/dist/engines/chant/types.js +38 -3
  81. package/dist/engines/harmonia/api.js +4 -0
  82. package/dist/engines/harmonia/data/doctrines.js +3 -1
  83. package/dist/engines/harmonia/tabula.d.ts +3 -0
  84. package/dist/engines/harmonia/tabula.js +1 -0
  85. package/dist/engines/harmonia/voice.d.ts +4 -0
  86. package/dist/engines/harmonia/voice.js +8 -4
  87. package/dist/engines/imprint.js +14 -1
  88. package/dist/engines/planet/orbital.js +4 -4
  89. package/dist/engines/planet/planet.d.ts +10 -0
  90. package/dist/engines/planet/planet.js +30 -3
  91. package/dist/engines/planet/position.js +13 -10
  92. package/dist/engines/planet/types.d.ts +1 -0
  93. package/dist/engines/score/api.d.ts +2 -13
  94. package/dist/engines/score/api.js +21 -8
  95. package/dist/engines/score/articulation.js +2 -2
  96. package/dist/engines/score/cadence.d.ts +76 -0
  97. package/dist/engines/score/cadence.js +96 -0
  98. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  99. package/dist/engines/score/emitters/accidentals.js +88 -0
  100. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  101. package/dist/engines/score/emitters/atramentum.js +239 -0
  102. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  103. package/dist/engines/score/emitters/breaking.js +80 -0
  104. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  105. package/dist/engines/score/emitters/moderna.js +612 -0
  106. package/dist/engines/score/emitters/svg.d.ts +143 -0
  107. package/dist/engines/score/emitters/svg.js +1328 -0
  108. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  109. package/dist/engines/score/emitters/tracks.js +728 -0
  110. package/dist/engines/score/infer.d.ts +3 -3
  111. package/dist/engines/score/infer.js +2 -2
  112. package/dist/engines/score/inscriptio.d.ts +69 -0
  113. package/dist/engines/score/inscriptio.js +138 -0
  114. package/dist/engines/score/ir.d.ts +2 -2
  115. package/dist/engines/score/ir.js +59 -12
  116. package/dist/engines/score/lyric.d.ts +23 -0
  117. package/dist/engines/score/lyric.js +234 -0
  118. package/dist/engines/score/meta.d.ts +2 -2
  119. package/dist/engines/score/modulation.d.ts +12 -0
  120. package/dist/engines/score/modulation.js +49 -0
  121. package/dist/engines/score/neume.js +35 -4
  122. package/dist/engines/score/parse.js +150 -9
  123. package/dist/engines/score/phrasing.js +4 -3
  124. package/dist/engines/score/prosody.d.ts +38 -0
  125. package/dist/engines/score/prosody.js +70 -6
  126. package/dist/engines/score/tabula.d.ts +37 -5
  127. package/dist/engines/score/tabula.js +18 -0
  128. package/dist/engines/score/types.d.ts +88 -1
  129. package/dist/engines/temper/api.d.ts +4 -1
  130. package/dist/engines/temper/api.js +28 -5
  131. package/dist/engines/temper/data/guido.js +6 -2
  132. package/dist/engines/temper/data/modes.d.ts +6 -0
  133. package/dist/engines/temper/data/modes.js +42 -0
  134. package/dist/engines/temper/data/tones.d.ts +1 -1
  135. package/dist/engines/temper/data/tones.js +20 -11
  136. package/dist/engines/temper/interval.js +4 -3
  137. package/dist/engines/temper/modality.d.ts +11 -2
  138. package/dist/engines/temper/modality.js +74 -2
  139. package/dist/engines/temper/modes.d.ts +1 -1
  140. package/dist/engines/temper/pitch.d.ts +1 -1
  141. package/dist/engines/temper/pitch.js +12 -2
  142. package/dist/engines/temper/scale.d.ts +53 -0
  143. package/dist/engines/temper/scale.js +107 -8
  144. package/dist/index.d.ts +28 -6
  145. package/dist/index.js +39 -3
  146. package/docs/api/calendar.md +279 -0
  147. package/docs/api/census.md +288 -0
  148. package/docs/api/chant.md +657 -0
  149. package/docs/api/heavens.md +346 -0
  150. package/docs/api/index.md +263 -0
  151. package/docs/api/score.md +873 -0
  152. package/docs/api/tuning.md +619 -0
  153. package/package.json +11 -5
  154. package/dist/data/office-psalms-roman.d.ts +0 -15
  155. package/dist/data/office-psalms-roman.js +0 -28
  156. package/dist/data/office-roman.d.ts +0 -19
  157. package/dist/data/office-roman.js +0 -13792
  158. package/dist/engines/score/emitters/midi.d.ts +0 -65
  159. package/dist/engines/score/emitters/midi.js +0 -162
  160. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  161. package/dist/engines/score/emitters/musicxml.js +0 -166
@@ -0,0 +1,107 @@
1
+ /** The one ink. Everything black is this black. */
2
+ export declare const INK = "#111";
3
+ /** The liturgical red. Reserved for the CLAIM lines: the mode's in the
4
+ * tonarium, and — ruled 2026-08-11, amending the 07-29 reservation — the
5
+ * word's accent in the prosodia. The precedent is the score's own text
6
+ * apparatus: the dropcap and annotations have always been rubricated, and
7
+ * in the books red is the word's colour. Nothing else wears it. */
8
+ export declare const RUBRICA = "#9E2B25";
9
+ /** Stratum opacities: one ink, graded. The melody strata (wave, spark) sit
10
+ * under their annotations; a claim re-inks at full strength. */
11
+ export declare const STRATUM: {
12
+ readonly wave: 0.75;
13
+ readonly spark: 0.45;
14
+ readonly cadence: 1;
15
+ readonly letters: 0.62;
16
+ readonly label: 0.9;
17
+ readonly bracket: 0.3;
18
+ readonly rail: 0.24;
19
+ readonly margin: 0.38;
20
+ readonly block: 0.18;
21
+ readonly rule: 0.15;
22
+ };
23
+ /** Below this confidence nothing is drawn. Not faint — absent. */
24
+ export declare const CONF_FLOOR = 0.45;
25
+ /** THE nib — one pressure law: normalized velocity → stroke width. */
26
+ export declare const nib: (vn: number) => number;
27
+ /** The stroke ladder. A diagram picks a rung; it does not invent a width.
28
+ * Named for what the line DOES, so the choice is about meaning, not weight. */
29
+ export declare const STROKE: {
30
+ readonly hair: 0.55;
31
+ readonly fine: 0.75;
32
+ readonly firm: 1.05;
33
+ readonly heavy: 1.5;
34
+ };
35
+ /** The type scale, and it is THE PAGE'S: a modular scale on 15px stepped by
36
+ * the Pythagorean minor third, 32:27 — the same six `--micro` … `--display-size`
37
+ * that ../../../../docs/styles.css declares. So a wheel's label and a table's
38
+ * label at the same step are the same size.
39
+ *
40
+ * It was 9/11/13.5/17/21/26, derived from what the diagrams happened to use.
41
+ * That was honest when the page had no scale of its own; once the page took
42
+ * one, the two agreed on `title` alone and every drawn label sat 1.5–2px under
43
+ * the prose around it. ../../../../scripts/vendor-ink.mjs asserts the pairing
44
+ * now, so this cannot drift again silently.
45
+ *
46
+ * Nothing inside tonus reads STEP — the site's diagrams are its only consumer
47
+ * — so moving it changes no emitted SVG for a package consumer. */
48
+ export declare const STEP: {
49
+ readonly micro: 10.5;
50
+ readonly caption: 12.5;
51
+ readonly label: 15;
52
+ readonly body: 18;
53
+ readonly title: 21;
54
+ readonly display: 25;
55
+ };
56
+ /** The house faces. Junicode is the serif — the real face, not the Crimson Pro
57
+ * stand-in the early rounds carried; Plex Mono is the machine register. */
58
+ export declare const HOUSE_SERIF = "Junicode, 'Crimson Pro', Georgia, serif";
59
+ /** The machine register. IBM Plex Mono FIRST: the site loads and self-hosts
60
+ * that face, and with `ui-monospace` at the head of the stack every drawn
61
+ * label resolved to the system mono — SF Mono on macOS — beside page text in
62
+ * Plex. Two monospaces, same size, same column. */
63
+ export declare const HOUSE_MONO = "'IBM Plex Mono', ui-monospace, Menlo, monospace";
64
+ /** The fourth stack, and NOT a fourth face: neither text face carries the
65
+ * planetary or musical signs (see fonts/README.md), so a figure that draws one
66
+ * falls back to the system. Mirrors `--symbol` in ../../../../docs/styles.css
67
+ * — the same sharing-by-numbers seam as HOUSE_SERIF and HOUSE_MONO, and it was duplicated
68
+ * inline in mutatio.js as a local SIGN_FACE before this existed. */
69
+ export declare const HOUSE_SYMBOL: string;
70
+ /** The figures a FIGURE sets: Junicode's own, which are OLDSTYLE by default —
71
+ * they sit on the baseline with ascenders and descenders as lowercase letters
72
+ * do, so a number reads as part of a line of text rather than standing off it.
73
+ *
74
+ * `features` is empty on purpose, and that is the finding worth keeping: in
75
+ * Junicode `zero` IS the oldstyle glyph and `zero.lf` is the lining variant,
76
+ * so `onum` maps lining BACK to the default and does nothing unless `lnum` is
77
+ * already on. Setting it here looked correct and changed no pixel — measured
78
+ * identical advances, 423.2 either way. Reach for `lnum`/`tnum` when a column
79
+ * of digits must line up; that is the setting that does work.
80
+ *
81
+ * Nothing in the library's own emitters uses this yet. It lives here because
82
+ * this file is the ONE definition of the ink system and the site vendors it
83
+ * (../../../../scripts/vendor-ink.mjs) — a second copy in the site would be
84
+ * the drift this module exists to prevent. */
85
+ export declare const FIGURES: {
86
+ readonly family: "Junicode, 'Crimson Pro', Georgia, serif";
87
+ readonly features: "";
88
+ };
89
+ /** A scaled measure, at most two places and no trailing zeros: 1.8, not 1.80. */
90
+ export declare const sc: (v: number) => string;
91
+ /** XML-escape a string for an SVG attribute or text node. */
92
+ export declare const esc: (s: string) => string;
93
+ export type Pt = [number, number];
94
+ /** Sample one cubic Bézier segment (matches the generators' tessellation). */
95
+ export declare function sampleCubic(p1: Pt, c1: Pt, c2: Pt, p2: Pt, steps: number): Pt[];
96
+ /** Catmull–Rom through the points, sampled — the one curve idiom. */
97
+ export declare function crSamples(pts: Pt[], steps: number): Pt[];
98
+ /** Piecewise-linear velocity read along x between anchors; the pressure signal
99
+ * every pressure-bearing line shares. Null velocities read 0.3. */
100
+ export declare function velocityAt(velpts: Pt[]): (x: number) => number;
101
+ /** The subject's own velocity ceiling — pressure normalizes per subject, not
102
+ * to a corpus constant (the plates' frozen 0.62 was a session artifact). */
103
+ export declare function velocityCeiling(velocities: readonly (number | null | undefined)[]): number;
104
+ /** A ribbon polygon around sampled points: THE nib at `scale`, velocity as
105
+ * width — the one pressure stroke everything draws with. */
106
+ export declare function ribbonPath(samples: Pt[], vat: (x: number) => number, vmax: number, scale: number): string;
107
+ //# sourceMappingURL=atramentum.d.ts.map
@@ -0,0 +1,239 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/score/emitters/atramentum — the ink: one drawing grammar
3
+ // ---------------------------------------------------------------------------
4
+ // THE GOVERNING INK SYSTEM (ruled 2026-07-29), extracted here so everything
5
+ // tonus draws shares one definition rather than agreeing by coincidence. The
6
+ // analysis tracks were its first consumer; the documentation site's diagrams
7
+ // are the second.
8
+ //
9
+ // - ONE INK. Every mark draws in the score's black; strata differ by OPACITY
10
+ // alone (STRATUM), never by hue. Rubrica is the only colour, and it is
11
+ // reserved for the claims: the mode line's in the tonarium, the accent's
12
+ // in the prosodia (ruled 2026-08-11, amending 07-29 — see RUBRICA below).
13
+ // - ONE NIB. Every pressure-bearing line shares one width law (`nib`): a
14
+ // normalized velocity becomes stroke width. Two marks at different opacities
15
+ // are the same stroke, not two different pens.
16
+ // - CONFIDENCE IS OPACITY, composing with the stratum: a weak claim fades, and
17
+ // below CONF_FLOOR it draws nothing at all. A thing tonus is unsure of does
18
+ // not get drawn faintly and hedged in a caption; it goes uninked.
19
+ //
20
+ // The design brief is "Dieter Rams designs a liturgical book" — functionalist
21
+ // restraint, one canonical accent, nothing decorative that is not also
22
+ // informative. STEP and STROKE exist so a diagram cannot quietly invent a
23
+ // fifteenth type size or a seventh hairline: it picks a step or it does not
24
+ // draw.
25
+ //
26
+ // ─── HOW THIS MEETS THE PAGE ───────────────────────────────────────────────
27
+ // The documentation site keeps a system of its own, stated at the top of
28
+ // ../../../../docs/styles.css. The two are separate and they AGREE BY SHARING
29
+ // NUMBERS,
30
+ // not by one importing the other:
31
+ //
32
+ // STEP mirrors the six CSS type steps (--micro … --display-size)
33
+ // INK mirrors --ink
34
+ // RUBRICA mirrors --rubrica
35
+ // HOUSE_* mirror --serif and --mono
36
+ //
37
+ // That seam is the thing to watch. A face or a step changed on one side and
38
+ // not the other does not fail — it renders, quietly, in the wrong thing. It
39
+ // has happened twice: HOUSE_MONO once listed ui-monospace first and drew
40
+ // every figure label in the system mono beside page text in Plex; HOUSE_SANS
41
+ // named a face no page loads, so the wheels' names and the tracks' letters
42
+ // came out in system-ui. Both are why the rule below is worth keeping literal.
43
+ //
44
+ // ─── THE FACES HAVE JOBS ───────────────────────────────────────────────────
45
+ // HOUSE_SERIF carries WORDS — a sign name, a month, a role, a gamut letter,
46
+ // a chironomy letter. HOUSE_MONO carries MACHINE DATA — hz, cents, ratios,
47
+ // ids. There is no third: if a label is a word, it is serif, wherever it is
48
+ // drawn. A figure that wants a face outside these two is asking the wrong
49
+ // question about its label.
50
+ //
51
+ // Colour reaches SVG the same way the score's does: a drawn figure names the
52
+ // site's token with its own value as the fallback — `var(--paper-dim,
53
+ // #F7F6F3)` — so the page can re-ink a figure it did not render, and the same
54
+ // figure standing alone still knows what it looks like.
55
+ //
56
+ // This module is INTERNAL. It is not part of the public API — the site reaches
57
+ // it through the vendored render subgraph, and outside consumers have no
58
+ // reason to draw in tonus's hand.
59
+ /** The one ink. Everything black is this black. */
60
+ export const INK = "#111";
61
+ /** The liturgical red. Reserved for the CLAIM lines: the mode's in the
62
+ * tonarium, and — ruled 2026-08-11, amending the 07-29 reservation — the
63
+ * word's accent in the prosodia. The precedent is the score's own text
64
+ * apparatus: the dropcap and annotations have always been rubricated, and
65
+ * in the books red is the word's colour. Nothing else wears it. */
66
+ export const RUBRICA = "#9E2B25";
67
+ /** Stratum opacities: one ink, graded. The melody strata (wave, spark) sit
68
+ * under their annotations; a claim re-inks at full strength. */
69
+ export const STRATUM = {
70
+ wave: 0.75, // the chironomy line — the gesture itself
71
+ spark: 0.45, // the tonarium melody — context, not message
72
+ cadence: 1.0, // the claim: the melody's ending, full ink
73
+ letters: 0.62, // Pierik letters
74
+ label: 0.9, // signature labels
75
+ bracket: 0.3, // the label's end-ticked tie
76
+ rail: 0.24, // the maneriae rails
77
+ margin: 0.38, // the "cad" margin word
78
+ block: 0.18, // the prosodia's melisma blocks — a soft fill, not a line
79
+ rule: 0.15, // the prosodia's single rule — quieter than a rail
80
+ };
81
+ /** Below this confidence nothing is drawn. Not faint — absent. */
82
+ export const CONF_FLOOR = 0.45;
83
+ /** THE nib — one pressure law: normalized velocity → stroke width. */
84
+ export const nib = (vn) => 0.5 + 1.5 * vn;
85
+ /** The stroke ladder. A diagram picks a rung; it does not invent a width.
86
+ * Named for what the line DOES, so the choice is about meaning, not weight. */
87
+ export const STROKE = {
88
+ hair: 0.55, // rails, grids, graticules — structure you read past
89
+ fine: 0.75, // the common diagram line
90
+ firm: 1.05, // a line making a claim (the mode line's weight)
91
+ heavy: 1.5, // an axis, a frame, a boundary
92
+ };
93
+ /** The type scale, and it is THE PAGE'S: a modular scale on 15px stepped by
94
+ * the Pythagorean minor third, 32:27 — the same six `--micro` … `--display-size`
95
+ * that ../../../../docs/styles.css declares. So a wheel's label and a table's
96
+ * label at the same step are the same size.
97
+ *
98
+ * It was 9/11/13.5/17/21/26, derived from what the diagrams happened to use.
99
+ * That was honest when the page had no scale of its own; once the page took
100
+ * one, the two agreed on `title` alone and every drawn label sat 1.5–2px under
101
+ * the prose around it. ../../../../scripts/vendor-ink.mjs asserts the pairing
102
+ * now, so this cannot drift again silently.
103
+ *
104
+ * Nothing inside tonus reads STEP — the site's diagrams are its only consumer
105
+ * — so moving it changes no emitted SVG for a package consumer. */
106
+ export const STEP = {
107
+ micro: 10.5, // superscripts, tick labels
108
+ caption: 12.5, // the workhorse — most diagram labels
109
+ label: 15, // named things
110
+ body: 18, // running text
111
+ title: 21, // a panel's name
112
+ display: 25, // the rare headline
113
+ };
114
+ /** The house faces. Junicode is the serif — the real face, not the Crimson Pro
115
+ * stand-in the early rounds carried; Plex Mono is the machine register. */
116
+ export const HOUSE_SERIF = "Junicode, 'Crimson Pro', Georgia, serif";
117
+ /* HOUSE_SANS is GONE. The house has three faces and each has a
118
+ * job: Junicode carries content and Latin, Plex Mono carries machine data,
119
+ * Jacquard is the wordmark. The sans stack named a fourth that no page loads,
120
+ * so every label reaching for it resolved to system-ui — SF Pro on macOS —
121
+ * and the wheels' names and the tracks' letters were set in it. Both now
122
+ * label in HOUSE_SERIF. Nothing should reintroduce a sans without a face to
123
+ * back it. */
124
+ /** The machine register. IBM Plex Mono FIRST: the site loads and self-hosts
125
+ * that face, and with `ui-monospace` at the head of the stack every drawn
126
+ * label resolved to the system mono — SF Mono on macOS — beside page text in
127
+ * Plex. Two monospaces, same size, same column. */
128
+ export const HOUSE_MONO = "'IBM Plex Mono', ui-monospace, Menlo, monospace";
129
+ /** The fourth stack, and NOT a fourth face: neither text face carries the
130
+ * planetary or musical signs (see fonts/README.md), so a figure that draws one
131
+ * falls back to the system. Mirrors `--symbol` in ../../../../docs/styles.css
132
+ * — the same sharing-by-numbers seam as HOUSE_SERIF and HOUSE_MONO, and it was duplicated
133
+ * inline in mutatio.js as a local SIGN_FACE before this existed. */
134
+ export const HOUSE_SYMBOL = "'Apple Symbols', 'Segoe UI Symbol', "
135
+ + "'Noto Sans Symbols2', " + HOUSE_SERIF;
136
+ /** The figures a FIGURE sets: Junicode's own, which are OLDSTYLE by default —
137
+ * they sit on the baseline with ascenders and descenders as lowercase letters
138
+ * do, so a number reads as part of a line of text rather than standing off it.
139
+ *
140
+ * `features` is empty on purpose, and that is the finding worth keeping: in
141
+ * Junicode `zero` IS the oldstyle glyph and `zero.lf` is the lining variant,
142
+ * so `onum` maps lining BACK to the default and does nothing unless `lnum` is
143
+ * already on. Setting it here looked correct and changed no pixel — measured
144
+ * identical advances, 423.2 either way. Reach for `lnum`/`tnum` when a column
145
+ * of digits must line up; that is the setting that does work.
146
+ *
147
+ * Nothing in the library's own emitters uses this yet. It lives here because
148
+ * this file is the ONE definition of the ink system and the site vendors it
149
+ * (../../../../scripts/vendor-ink.mjs) — a second copy in the site would be
150
+ * the drift this module exists to prevent. */
151
+ export const FIGURES = {
152
+ family: HOUSE_SERIF,
153
+ features: "",
154
+ };
155
+ /** A scaled measure, at most two places and no trailing zeros: 1.8, not 1.80. */
156
+ export const sc = (v) => Number(v.toFixed(2)).toString();
157
+ /** XML-escape a string for an SVG attribute or text node. */
158
+ export const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
159
+ .replace(/"/g, "&quot;");
160
+ /** Sample one cubic Bézier segment (matches the generators' tessellation). */
161
+ export function sampleCubic(p1, c1, c2, p2, steps) {
162
+ const out = [];
163
+ for (let k = 0; k < steps; k++) {
164
+ const t = k / steps;
165
+ const mt = 1 - t;
166
+ out.push([
167
+ mt ** 3 * p1[0] + 3 * mt * mt * t * c1[0] + 3 * mt * t * t * c2[0] + t ** 3 * p2[0],
168
+ mt ** 3 * p1[1] + 3 * mt * mt * t * c1[1] + 3 * mt * t * t * c2[1] + t ** 3 * p2[1],
169
+ ]);
170
+ }
171
+ return out;
172
+ }
173
+ /** Catmull–Rom through the points, sampled — the one curve idiom. */
174
+ export function crSamples(pts, steps) {
175
+ if (pts.length < 2)
176
+ return [...pts];
177
+ const P = [pts[0], ...pts, pts[pts.length - 1]];
178
+ const out = [];
179
+ for (let i = 1; i < P.length - 2; i++) {
180
+ const [p0, p1, p2, p3] = [P[i - 1], P[i], P[i + 1], P[i + 2]];
181
+ const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
182
+ const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
183
+ out.push(...sampleCubic(p1, c1, c2, p2, steps));
184
+ }
185
+ out.push(pts[pts.length - 1]);
186
+ return out;
187
+ }
188
+ /** Piecewise-linear velocity read along x between anchors; the pressure signal
189
+ * every pressure-bearing line shares. Null velocities read 0.3. */
190
+ export function velocityAt(velpts) {
191
+ return (x) => {
192
+ if (velpts.length === 0)
193
+ return 0.3;
194
+ if (x <= velpts[0][0])
195
+ return velpts[0][1];
196
+ if (x >= velpts[velpts.length - 1][0])
197
+ return velpts[velpts.length - 1][1];
198
+ for (let i = 0; i + 1 < velpts.length; i++) {
199
+ const [x0, v0] = velpts[i];
200
+ const [x1, v1] = velpts[i + 1];
201
+ if (x0 <= x && x <= x1)
202
+ return v0 + (v1 - v0) * ((x - x0) / Math.max(x1 - x0, 1e-6));
203
+ }
204
+ return 0.3;
205
+ };
206
+ }
207
+ /** The subject's own velocity ceiling — pressure normalizes per subject, not
208
+ * to a corpus constant (the plates' frozen 0.62 was a session artifact). */
209
+ export function velocityCeiling(velocities) {
210
+ let vmax = 0;
211
+ for (const v of velocities)
212
+ if (v != null && v > vmax)
213
+ vmax = v;
214
+ return vmax > 0 ? vmax : 0.62;
215
+ }
216
+ /** A ribbon polygon around sampled points: THE nib at `scale`, velocity as
217
+ * width — the one pressure stroke everything draws with. */
218
+ export function ribbonPath(samples, vat, vmax, scale) {
219
+ const top = [];
220
+ const bot = [];
221
+ const N = samples.length;
222
+ for (let i = 0; i < N; i++) {
223
+ const [x, y] = samples[i];
224
+ const [x0, y0] = samples[Math.max(i - 1, 0)];
225
+ const [x1, y1] = samples[Math.min(i + 1, N - 1)];
226
+ const dx = x1 - x0;
227
+ const dy = y1 - y0;
228
+ const L = Math.hypot(dx, dy) || 1;
229
+ const nx = -dy / L;
230
+ const ny = dx / L;
231
+ const vn = Math.min(vat(x) / vmax, 1);
232
+ const w = nib(vn) * scale;
233
+ top.push([x + (nx * w) / 2, y + (ny * w) / 2]);
234
+ bot.push([x - (nx * w) / 2, y - (ny * w) / 2]);
235
+ }
236
+ return "M " + top.map((p) => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" L ") +
237
+ " L " + bot.reverse().map((p) => `${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" L ") + " Z";
238
+ }
239
+ //# sourceMappingURL=atramentum.js.map
@@ -0,0 +1,62 @@
1
+ import type { ChantTabulaRow } from "../tabula.js";
2
+ /** What the caller knows at a candidate break point. */
3
+ export interface BreakQuery {
4
+ /** The row that would OPEN the next system. */
5
+ next: ChantTabulaRow;
6
+ /** Cursor position, in svg user units. */
7
+ x: number;
8
+ /** Rightmost usable x — the line's width less padding and any line-end mark. */
9
+ boundary: number;
10
+ /**
11
+ * Width of what must fit before the next legal break point: a phrase for
12
+ * quadrata, a syllable for moderna. Measured by the caller, never estimated
13
+ * here.
14
+ */
15
+ need: number;
16
+ /**
17
+ * Width of the whole sealed run starting at `next`, when `next.keepWithPrev`
18
+ * is set — the caller measures to the end of the `<nlba>` group. Ignored
19
+ * otherwise.
20
+ */
21
+ sealedRun?: number;
22
+ /** Where a fresh system's cursor starts, for judging whether a run fits at all. */
23
+ lineStart: number;
24
+ /**
25
+ * Set when the caller has ALREADY consumed `next.lineBreak` itself. Quadrata
26
+ * honours `z` in its own block (it must repeat the clef and place a custos
27
+ * before the staff advances), so asking here too would break the same system
28
+ * twice — measured, one Graduale chant drew 9 custos for 9 systems where 8 is
29
+ * correct, the last system needing none.
30
+ */
31
+ forcedHandled?: boolean;
32
+ }
33
+ /** Why a system ended — carried so a caller can vary the line-end mark. */
34
+ export type BreakReason = "forced" | "width" | "none";
35
+ export interface BreakVerdict {
36
+ break: boolean;
37
+ reason: BreakReason;
38
+ }
39
+ /**
40
+ * Decide whether a system ends before `next`.
41
+ *
42
+ * The order is the precedence order, and it is the whole contract:
43
+ *
44
+ * 1. `z` — an instruction, not a preference. Nothing overrides it, including
45
+ * a seal: an editor who wrote both meant both, and the break is the more
46
+ * specific statement.
47
+ * 2. `<nlba>` — forbids a break, unless honouring it would push the sealed run
48
+ * off the page. A seal is a preference about where a line ends; staying
49
+ * inside the canvas is the stronger claim.
50
+ * 3. Width — the ordinary case.
51
+ */
52
+ export declare function decideBreak(q: BreakQuery): BreakVerdict;
53
+ /**
54
+ * Width of the sealed run beginning at `from`, by the caller's own measure.
55
+ *
56
+ * The head of a sealed group must be tested against the WHOLE group: admit the
57
+ * head alone and every seam after it is sealed, so the line can no longer break
58
+ * and the overflow is unrecoverable. Measured across the 35 Graduale chants that
59
+ * carry `<nlba>`, measuring one syllable instead of the run broke 37 seams.
60
+ */
61
+ export declare function sealedRunWidth(rows: ChantTabulaRow[], from: number, widthOf: (index: number) => number): number;
62
+ //# sourceMappingURL=breaking.d.ts.map
@@ -0,0 +1,80 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/score/emitters/breaking — where a system ends, for both species
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // The two emitters break lines at different granularities — quadrata at a
6
+ // divisio (and, since 2026-08-05, at a word), moderna between syllables — and
7
+ // they draw different line-end marks (quadrata a custos, moderna none). Those
8
+ // are real differences between the duae species, not accidents to unify.
9
+ //
10
+ // But the RULES governing a break are the same in both, and were written twice:
11
+ //
12
+ // - `z` forces a break, outranking any width test (the engraver's own)
13
+ // - `<nlba>` seals a seam and forbids one (the editor's own)
14
+ // - a seal yields when the sealed run cannot fit a line (the page wins)
15
+ // - the usable width reserves the line-end mark (never overrun it)
16
+ //
17
+ // Writing them twice is what shipped `<nlba>` working in quadrata and broken in
18
+ // moderna on 2026-08-05: the fix went into the emitter I happened to patch
19
+ // first, and only a sweep across both species caught it. This module is the
20
+ // mechanism; each emitter keeps its own policy and its own drawing.
21
+ //
22
+ // Nothing here measures or draws. The caller supplies measurements — which is
23
+ // what keeps tonus headless: the estimate lives in the emitter, the decision
24
+ // lives here, and neither needs a DOM.
25
+ const NO_BREAK = { break: false, reason: "none" };
26
+ /**
27
+ * Decide whether a system ends before `next`.
28
+ *
29
+ * The order is the precedence order, and it is the whole contract:
30
+ *
31
+ * 1. `z` — an instruction, not a preference. Nothing overrides it, including
32
+ * a seal: an editor who wrote both meant both, and the break is the more
33
+ * specific statement.
34
+ * 2. `<nlba>` — forbids a break, unless honouring it would push the sealed run
35
+ * off the page. A seal is a preference about where a line ends; staying
36
+ * inside the canvas is the stronger claim.
37
+ * 3. Width — the ordinary case.
38
+ */
39
+ export function decideBreak(q) {
40
+ if (q.next.lineBreak && !q.forcedHandled)
41
+ return { break: true, reason: "forced" };
42
+ if (q.next.keepWithPrev) {
43
+ const run = q.sealedRun ?? q.need;
44
+ // A run that cannot fit a line of its own has nowhere better to go, so the
45
+ // seal yields rather than running off the canvas. Otherwise the seal holds:
46
+ // the break belongs before the group, and the caller finds it there.
47
+ const fitsAnyLine = q.lineStart + run <= q.boundary;
48
+ if (fitsAnyLine)
49
+ return NO_BREAK;
50
+ }
51
+ // Past the boundary already — break regardless of what comes next. This must
52
+ // be tested on its own: a single figure wider than a whole line (a 36-note
53
+ // melisma at 420px) can never be rescued by breaking, but the line before it
54
+ // should still end rather than run on. Folding this into the `need` test
55
+ // below let such a syllable extend a line that was already full — measured,
56
+ // one Graduale render overran by 56px.
57
+ if (q.x > q.boundary)
58
+ return { break: true, reason: "width" };
59
+ return q.x + q.need > q.boundary
60
+ ? { break: true, reason: "width" }
61
+ : NO_BREAK;
62
+ }
63
+ /**
64
+ * Width of the sealed run beginning at `from`, by the caller's own measure.
65
+ *
66
+ * The head of a sealed group must be tested against the WHOLE group: admit the
67
+ * head alone and every seam after it is sealed, so the line can no longer break
68
+ * and the overflow is unrecoverable. Measured across the 35 Graduale chants that
69
+ * carry `<nlba>`, measuring one syllable instead of the run broke 37 seams.
70
+ */
71
+ export function sealedRunWidth(rows, from, widthOf) {
72
+ let total = widthOf(from);
73
+ for (let i = from + 1; i < rows.length; i++) {
74
+ if (!rows[i].keepWithPrev)
75
+ break;
76
+ total += widthOf(i);
77
+ }
78
+ return total;
79
+ }
80
+ //# sourceMappingURL=breaking.js.map
@@ -0,0 +1,38 @@
1
+ import type { SvgResult, SvgOpts } from "./svg.js";
2
+ import type { ChantTabulaRow } from "../tabula.js";
3
+ import type { Chant } from "../../chant/types.js";
4
+ /** Every geometric constant, derived from the requested staff height. */
5
+ export interface ModernaMetrics {
6
+ /** Staff space — the distance between two lines. */
7
+ MSP: number;
8
+ /** SMuFL glyph scale: 1 em = 4 staff spaces. */
9
+ SCALE: number;
10
+ /** Top staff line, system-local. */
11
+ MTOP: number;
12
+ /** noteheadBlack advance. */
13
+ NH_W: number;
14
+ /** Per-note advance inside a melisma. */
15
+ ADV: number;
16
+ /** Gap after each syllable. */
17
+ SYL_GAP: number;
18
+ /** Lyric baseline, system-local. */
19
+ LYRIC_Y: number;
20
+ /** The factor against the engraved metrics — 1 at the default. */
21
+ k: number;
22
+ }
23
+ /**
24
+ * Moderna's metrics for a requested staff height.
25
+ *
26
+ * Everything scales together, so the engraving's internal relationships —
27
+ * notehead to staff, slur to notehead, lyric to baseline — hold at any size;
28
+ * they were drawn as a set and stay one.
29
+ */
30
+ export declare function metrics(staffHeight?: number): ModernaMetrics;
31
+ type Row = ChantTabulaRow;
32
+ /**
33
+ * Render the tabula as moderna. Multi-system when `width` is set; single system
34
+ * otherwise. Returns the shared { svg, geometry } contract.
35
+ */
36
+ export declare function toModerna(rows: Row[], chant: Chant, options?: SvgOpts): SvgResult;
37
+ export {};
38
+ //# sourceMappingURL=moderna.d.ts.map