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.
- package/BIBLIOGRAPHY.md +132 -108
- package/CHANGELOG.md +610 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2669 -11196
- package/dist/data/ams.d.ts +5 -0
- package/dist/data/ams.js +122 -0
- package/dist/data/attestation.d.ts +19 -0
- package/dist/data/attestation.js +15716 -0
- package/dist/data/attestation.json +15711 -0
- package/dist/data/cadentiae.d.ts +43 -0
- package/dist/data/cadentiae.js +174 -0
- package/dist/data/cal.js +48 -0
- package/dist/data/census.d.ts +12 -0
- package/dist/data/census.js +36 -0
- package/dist/data/commune-office.d.ts +4 -0
- package/dist/data/commune-office.js +2371 -0
- package/dist/data/commune-office.json +2365 -0
- package/dist/data/commune.js +181 -11
- package/dist/data/corpus-overlap.d.ts +24 -0
- package/dist/data/corpus-overlap.js +347 -0
- package/dist/data/cot.d.ts +5 -0
- package/dist/data/cot.js +172 -0
- package/dist/data/cse.d.ts +5 -0
- package/dist/data/cse.js +122 -0
- package/dist/data/gabc-glyphs.d.ts +45 -0
- package/dist/data/gabc-glyphs.js +122 -0
- package/dist/data/gr.js +757 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +802 -13419
- package/dist/data/lh.js +116 -3473
- package/dist/data/lu.js +935 -17632
- package/dist/data/nocturnale-romanum.d.ts +5 -0
- package/dist/data/nocturnale-romanum.js +3772 -0
- package/dist/data/office-ferial.d.ts +4 -0
- package/dist/data/office-ferial.js +396 -0
- package/dist/data/office-ferial.json +391 -0
- package/dist/data/office-monastic.d.ts +17 -1
- package/dist/data/office-monastic.js +1403 -466
- package/dist/data/office-psalms-monastic.d.ts +13 -1
- package/dist/data/office-psalms-monastic.js +9 -0
- package/dist/data/propers.js +1 -1
- package/dist/data/psalms.js +22919 -5
- package/dist/data/psm.d.ts +5 -0
- package/dist/data/psm.js +122 -0
- package/dist/data/seasonal-respbreve.d.ts +5 -0
- package/dist/data/seasonal-respbreve.js +41 -0
- package/dist/data/seasonal-respbreve.json +35 -0
- package/dist/data/smufl-glyphs.d.ts +17 -0
- package/dist/data/smufl-glyphs.js +1546 -0
- package/dist/data/smufl-glyphs.json +1530 -0
- package/dist/engines/cal/calendar.d.ts +3 -2
- package/dist/engines/cal/calendar.js +105 -29
- package/dist/engines/cal/data/eras.d.ts +35 -0
- package/dist/engines/cal/data/eras.js +128 -0
- package/dist/engines/cal/date.js +44 -0
- package/dist/engines/cal/types.d.ts +15 -3
- package/dist/engines/cal/types.js +5 -5
- package/dist/engines/census/census.d.ts +7 -0
- package/dist/engines/census/census.js +179 -0
- package/dist/engines/census/types.d.ts +55 -0
- package/dist/engines/census/types.js +8 -0
- package/dist/engines/chant/attest.d.ts +39 -0
- package/dist/engines/chant/attest.js +90 -0
- package/dist/engines/chant/chant.d.ts +22 -4
- package/dist/engines/chant/chant.js +274 -14
- package/dist/engines/chant/data/compline.js +2 -1
- package/dist/engines/chant/data/masses.d.ts +56 -4
- package/dist/engines/chant/data/masses.js +305 -80
- package/dist/engines/chant/data/prime.js +1 -1
- package/dist/engines/chant/hour.js +279 -58
- package/dist/engines/chant/ordinary.d.ts +2 -0
- package/dist/engines/chant/ordinary.js +336 -56
- package/dist/engines/chant/propers.js +55 -5
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +25 -11
- package/dist/engines/chant/syllabify.d.ts +1 -0
- package/dist/engines/chant/syllabify.js +90 -17
- package/dist/engines/chant/types.d.ts +145 -10
- package/dist/engines/chant/types.js +38 -3
- package/dist/engines/harmonia/api.js +4 -0
- package/dist/engines/harmonia/data/doctrines.js +3 -1
- package/dist/engines/harmonia/tabula.d.ts +3 -0
- package/dist/engines/harmonia/tabula.js +1 -0
- package/dist/engines/harmonia/voice.d.ts +4 -0
- package/dist/engines/harmonia/voice.js +8 -4
- package/dist/engines/imprint.js +14 -1
- package/dist/engines/planet/orbital.js +4 -4
- package/dist/engines/planet/planet.d.ts +10 -0
- package/dist/engines/planet/planet.js +30 -3
- package/dist/engines/planet/position.js +13 -10
- package/dist/engines/planet/types.d.ts +1 -0
- package/dist/engines/score/api.d.ts +2 -13
- package/dist/engines/score/api.js +21 -8
- package/dist/engines/score/articulation.js +2 -2
- package/dist/engines/score/cadence.d.ts +76 -0
- package/dist/engines/score/cadence.js +96 -0
- package/dist/engines/score/emitters/accidentals.d.ts +21 -0
- package/dist/engines/score/emitters/accidentals.js +88 -0
- package/dist/engines/score/emitters/atramentum.d.ts +107 -0
- package/dist/engines/score/emitters/atramentum.js +239 -0
- package/dist/engines/score/emitters/breaking.d.ts +62 -0
- package/dist/engines/score/emitters/breaking.js +80 -0
- package/dist/engines/score/emitters/moderna.d.ts +38 -0
- package/dist/engines/score/emitters/moderna.js +612 -0
- package/dist/engines/score/emitters/svg.d.ts +143 -0
- package/dist/engines/score/emitters/svg.js +1328 -0
- package/dist/engines/score/emitters/tracks.d.ts +104 -0
- package/dist/engines/score/emitters/tracks.js +728 -0
- package/dist/engines/score/infer.d.ts +3 -3
- package/dist/engines/score/infer.js +2 -2
- package/dist/engines/score/inscriptio.d.ts +69 -0
- package/dist/engines/score/inscriptio.js +138 -0
- package/dist/engines/score/ir.d.ts +2 -2
- package/dist/engines/score/ir.js +59 -12
- package/dist/engines/score/lyric.d.ts +23 -0
- package/dist/engines/score/lyric.js +234 -0
- package/dist/engines/score/meta.d.ts +2 -2
- package/dist/engines/score/modulation.d.ts +12 -0
- package/dist/engines/score/modulation.js +49 -0
- package/dist/engines/score/neume.js +35 -4
- package/dist/engines/score/parse.js +150 -9
- package/dist/engines/score/phrasing.js +4 -3
- package/dist/engines/score/prosody.d.ts +38 -0
- package/dist/engines/score/prosody.js +70 -6
- package/dist/engines/score/tabula.d.ts +37 -5
- package/dist/engines/score/tabula.js +18 -0
- package/dist/engines/score/types.d.ts +88 -1
- package/dist/engines/temper/api.d.ts +4 -1
- package/dist/engines/temper/api.js +28 -5
- package/dist/engines/temper/data/guido.js +6 -2
- package/dist/engines/temper/data/modes.d.ts +6 -0
- package/dist/engines/temper/data/modes.js +42 -0
- package/dist/engines/temper/data/tones.d.ts +1 -1
- package/dist/engines/temper/data/tones.js +20 -11
- package/dist/engines/temper/interval.js +4 -3
- package/dist/engines/temper/modality.d.ts +11 -2
- package/dist/engines/temper/modality.js +74 -2
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/engines/temper/pitch.d.ts +1 -1
- package/dist/engines/temper/pitch.js +12 -2
- package/dist/engines/temper/scale.d.ts +53 -0
- package/dist/engines/temper/scale.js +107 -8
- package/dist/index.d.ts +28 -6
- package/dist/index.js +39 -3
- package/docs/api/calendar.md +279 -0
- package/docs/api/census.md +288 -0
- package/docs/api/chant.md +657 -0
- package/docs/api/heavens.md +346 -0
- package/docs/api/index.md +263 -0
- package/docs/api/score.md +873 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +11 -5
- package/dist/data/office-psalms-roman.d.ts +0 -15
- package/dist/data/office-psalms-roman.js +0 -28
- package/dist/data/office-roman.d.ts +0 -19
- package/dist/data/office-roman.js +0 -13792
- package/dist/engines/score/emitters/midi.d.ts +0 -65
- package/dist/engines/score/emitters/midi.js +0 -162
- package/dist/engines/score/emitters/musicxml.d.ts +0 -18
- 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, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
159
|
+
.replace(/"/g, """);
|
|
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
|