tonus 0.1.8 → 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 +138 -108
- package/CHANGELOG.md +664 -1
- package/LICENSE +133 -29
- package/README.md +107 -80
- package/dist/data/am.js +2666 -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 +17 -0
- package/dist/data/corpus-overlap.js +299 -5
- 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 +754 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +799 -13419
- package/dist/data/lh.js +113 -3473
- package/dist/data/lu.js +931 -17631
- package/dist/data/nocturnale-romanum.js +1659 -10411
- 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 +16 -4
- package/dist/engines/chant/chant.js +220 -30
- 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 +115 -12
- 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/data/zodiac.d.ts +47 -0
- package/dist/engines/harmonia/data/zodiac.js +163 -0
- 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 +1335 -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 +26 -8
- package/dist/index.js +40 -4
- 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 +396 -0
- package/docs/api/index.md +265 -0
- package/docs/api/score.md +906 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +13 -5
- package/dist/data/office-matins-roman.d.ts +0 -19
- package/dist/data/office-matins-roman.js +0 -4383
- 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/chant/matutinum.d.ts +0 -33
- package/dist/engines/chant/matutinum.js +0 -81
- 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
|
@@ -125,6 +125,84 @@ function bestFigure(observed, figures) {
|
|
|
125
125
|
}
|
|
126
126
|
return best;
|
|
127
127
|
}
|
|
128
|
+
// ── The corpus catalogue (CADENTIAE) ────────────────────────────────────────
|
|
129
|
+
// The mining keyed tails by their last <=4 notes; the classifier reads the same
|
|
130
|
+
// span out of the (longer) formula window.
|
|
131
|
+
const TAIL = 4;
|
|
132
|
+
/**
|
|
133
|
+
* Octave-reduce a semitone offset to [-5..+6].
|
|
134
|
+
*
|
|
135
|
+
* NOT part of the family key — kept because the folded value is still worth
|
|
136
|
+
* reporting (it is the scale DEGREE, mode-theoretically real). As a key the
|
|
137
|
+
* fold made a fifth ABOVE the final share a family with a fourth BELOW:
|
|
138
|
+
* measured over 27,985 phrase ends, 3,499 landed on @-5, of which 2,427 were
|
|
139
|
+
* really +7 and 1,072 really -5. Two opposite gestures, one key. Arrival in
|
|
140
|
+
* the key is therefore the SIGNED offset; see cadenceKeys().
|
|
141
|
+
*/
|
|
142
|
+
export function reduceArrival(semitones) {
|
|
143
|
+
let a = semitones % 12;
|
|
144
|
+
if (a > 6)
|
|
145
|
+
a -= 12;
|
|
146
|
+
if (a < -5)
|
|
147
|
+
a += 12;
|
|
148
|
+
return a;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* THE cadence family key — one implementation, shared by every consumer.
|
|
152
|
+
*
|
|
153
|
+
* This once existed three times: here (keyed off the Phrase tree), in
|
|
154
|
+
* tonus-corpus `census/_shared.mjs` (keyed off flat tabula rows), and in the
|
|
155
|
+
* CADENTIAE miner (a character-for-character copy of the census one). They
|
|
156
|
+
* agreed — measured corpus-wide, 28,051 engine cadences against 27,985 census
|
|
157
|
+
* phrase ends with ZERO key disagreements — but agreement by luck across three
|
|
158
|
+
* copies is what "no second parser, no drift" forbids; hence this one shared
|
|
159
|
+
* function.
|
|
160
|
+
*
|
|
161
|
+
* Takes the FLAT shape, because that is what the census and the miner have; the
|
|
162
|
+
* engine's own detection flattens into it. A phrase end is a `phraseIndex`
|
|
163
|
+
* transition or the last row.
|
|
164
|
+
*
|
|
165
|
+
* A ONE-NOTE PHRASE IS A CADENCE — it has a landing but no gesture, so it is
|
|
166
|
+
* emitted with an empty shape rather than skipped. (The census once dropped
|
|
167
|
+
* these — 68 corpus-wide, all real phrases carrying a real divisio, mostly
|
|
168
|
+
* "::" at the chant end — a hole this shared key closes.)
|
|
169
|
+
*/
|
|
170
|
+
export function cadenceKeys(rows, finalMidi) {
|
|
171
|
+
if (!rows.length)
|
|
172
|
+
return [];
|
|
173
|
+
const final = finalMidi ?? rows[rows.length - 1].midi;
|
|
174
|
+
const events = [];
|
|
175
|
+
for (let i = 0; i < rows.length; i++) {
|
|
176
|
+
const next = rows[i + 1];
|
|
177
|
+
if (next && next.phraseIndex === rows[i].phraseIndex)
|
|
178
|
+
continue; // not a phrase end
|
|
179
|
+
// The last <=TAIL rows of this phrase, ending at row i.
|
|
180
|
+
const seg = [];
|
|
181
|
+
for (let j = i; j >= 0 && seg.length < TAIL && rows[j].phraseIndex === rows[i].phraseIndex; j--) {
|
|
182
|
+
seg.unshift(rows[j]);
|
|
183
|
+
}
|
|
184
|
+
const shape = seg.slice(1).map((r, k) => r.midi - seg[k].midi);
|
|
185
|
+
const arrival = seg[seg.length - 1].midi - final;
|
|
186
|
+
events.push({
|
|
187
|
+
key: `${shape.join(",")} @${arrival}`,
|
|
188
|
+
shape,
|
|
189
|
+
arrival,
|
|
190
|
+
degree: reduceArrival(arrival),
|
|
191
|
+
isFinal: rows[i].divisio === "::" || !next,
|
|
192
|
+
phraseIndex: rows[i].phraseIndex,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return events;
|
|
196
|
+
}
|
|
197
|
+
/** The chant's closing note — the reference every arrival is measured from. */
|
|
198
|
+
function chantFinalMidi(phrases) {
|
|
199
|
+
for (let pi = phrases.length - 1; pi >= 0; pi--) {
|
|
200
|
+
const w = phraseFinalWindow(phrases[pi]);
|
|
201
|
+
if (w.length > 0)
|
|
202
|
+
return w[w.length - 1].midi;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
128
206
|
/**
|
|
129
207
|
* Detect the cadence closing each phrase. One Cadence per phrase that carries a
|
|
130
208
|
* divisio. With no mode, targets/approach are still classified but no figure is
|
|
@@ -132,6 +210,7 @@ function bestFigure(observed, figures) {
|
|
|
132
210
|
*/
|
|
133
211
|
export function detectCadences(phrases, modeData) {
|
|
134
212
|
const cadences = [];
|
|
213
|
+
const finalMidi = chantFinalMidi(phrases);
|
|
135
214
|
for (let pi = 0; pi < phrases.length; pi++) {
|
|
136
215
|
const phrase = phrases[pi];
|
|
137
216
|
if (!phrase.divisio)
|
|
@@ -162,6 +241,15 @@ export function detectCadences(phrases, modeData) {
|
|
|
162
241
|
}
|
|
163
242
|
}
|
|
164
243
|
}
|
|
244
|
+
// The corpus classification, from THE shared key function — the engine does
|
|
245
|
+
// not compute this itself. `cadenceKeys` takes flat rows, so the window is
|
|
246
|
+
// projected into that shape; one phrase in, one event out.
|
|
247
|
+
const ev = cadenceKeys(window.map((w) => ({ phraseIndex: 0, midi: w.midi, divisio: null })), finalMidi)[0];
|
|
248
|
+
const shape = ev?.shape ?? [];
|
|
249
|
+
const arrival = ev?.arrival ?? 0;
|
|
250
|
+
// A 1-note phrase has a landing but no gesture: a real cadence with an
|
|
251
|
+
// empty shape, which is why `signature` is the key rather than null.
|
|
252
|
+
const signature = ev?.key ?? null;
|
|
165
253
|
cadences.push({
|
|
166
254
|
phraseIndex: pi,
|
|
167
255
|
divisio,
|
|
@@ -172,6 +260,14 @@ export function detectCadences(phrases, modeData) {
|
|
|
172
260
|
steps,
|
|
173
261
|
confidence: Math.round(confidence * 100) / 100,
|
|
174
262
|
notes: window.map((w) => [pi, w.syllableIndex, w.noteIndex]),
|
|
263
|
+
signature,
|
|
264
|
+
shape,
|
|
265
|
+
arrival,
|
|
266
|
+
// Left null here on purpose. Detection is a pure pass over the phrase
|
|
267
|
+
// tree; the corpus catalogue is generated data, and reaching for it from
|
|
268
|
+
// inside the detector would put a baked artifact in the detection path.
|
|
269
|
+
// buildScore joins it, where MODES is already joined.
|
|
270
|
+
finality: null,
|
|
175
271
|
});
|
|
176
272
|
}
|
|
177
273
|
return cadences;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChantTabulaRow } from "../tabula.js";
|
|
2
|
+
export type AccidentalMode = "standard" | "heji" | "cents";
|
|
3
|
+
export type CentsBaseline = "pythagorean" | "et";
|
|
4
|
+
/** One note's intonation mark, for the emitter to place before/above the head. */
|
|
5
|
+
export interface AccidentalMark {
|
|
6
|
+
/** "glyph" → a notehead-preceding accidental; "cents" → a label floating above the staff. */
|
|
7
|
+
kind: "glyph" | "cents";
|
|
8
|
+
/** Glyph codepoint (kind "glyph") — a standard or HEJI accidental. */
|
|
9
|
+
glyph?: string;
|
|
10
|
+
/** Text label (kind "cents") — e.g. "−3.9". */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
export type AccidentalGlyphSet = "modern" | "medieval";
|
|
14
|
+
/**
|
|
15
|
+
* Compute the intonation mark for each tabula row, or null where none applies.
|
|
16
|
+
* Repeat-suppression (standard mode) and per-phrase pitch-class suppression
|
|
17
|
+
* (cents mode) keep the staff from being carpeted.
|
|
18
|
+
* @throws Error when `heji` is asked of a non-just (meantone) tuning.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeAccidentals(rows: ChantTabulaRow[], mode: AccidentalMode, centsBaseline?: CentsBaseline, glyphSet?: AccidentalGlyphSet): (AccidentalMark | null)[];
|
|
21
|
+
//# sourceMappingURL=accidentals.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { pythagoreanCentsByPc } from "../../temper/scale.js";
|
|
2
|
+
// The Pythagorean chain's ET-cents deviation per pitch class, derived from the
|
|
3
|
+
// SAME E♭–G♯ chain the engine tunes with (temper/scale.ts) so baseline and
|
|
4
|
+
// engine can never disagree — a hardcoded copy once drifted and broke the
|
|
5
|
+
// heji channel for every flatted chant under the default tuning.
|
|
6
|
+
const PYTH_BASELINE = pythagoreanCentsByPc();
|
|
7
|
+
const GLYPHS_BY_SET = {
|
|
8
|
+
modern: { [-1]: "E260", 0: "E261", 1: "E262" },
|
|
9
|
+
medieval: { [-1]: "E9E0", 0: "E9E1", 1: "E9E3" },
|
|
10
|
+
};
|
|
11
|
+
// HEJI comma glyphs (Extended Helmholtz–Ellis, U+E2C0–). The syntonic-comma
|
|
12
|
+
// arrows: one comma down / up. Higher-order commas are out of scope for 0.2.
|
|
13
|
+
const HEJI_COMMA_DOWN = "E2C2"; // one syntonic comma lower
|
|
14
|
+
const HEJI_COMMA_UP = "E2C7"; // one syntonic comma higher
|
|
15
|
+
/** Deviation of a note from the Pythagorean chain, in cents. */
|
|
16
|
+
function fromPythagorean(row) {
|
|
17
|
+
return row.offset - (PYTH_BASELINE[row.pc] ?? 0);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Compute the intonation mark for each tabula row, or null where none applies.
|
|
21
|
+
* Repeat-suppression (standard mode) and per-phrase pitch-class suppression
|
|
22
|
+
* (cents mode) keep the staff from being carpeted.
|
|
23
|
+
* @throws Error when `heji` is asked of a non-just (meantone) tuning.
|
|
24
|
+
*/
|
|
25
|
+
export function computeAccidentals(rows, mode, centsBaseline = "pythagorean", glyphSet = "modern") {
|
|
26
|
+
if (mode === "standard") {
|
|
27
|
+
// A glyph before any note whose pitch is explicitly inflected — but not on
|
|
28
|
+
// an immediately-repeated same pitch (restate only after another pitch).
|
|
29
|
+
let prevPc = null;
|
|
30
|
+
return rows.map((row) => {
|
|
31
|
+
const repeat = row.pc === prevPc;
|
|
32
|
+
prevPc = row.pc;
|
|
33
|
+
if (row.accidentalSource !== "explicit" || repeat)
|
|
34
|
+
return null;
|
|
35
|
+
// Draw the SIGN the source wrote, not this note's own alteration. In
|
|
36
|
+
// `fe(jx)cit(ih)` the flat is printed before the I while it governs J —
|
|
37
|
+
// where a sign is printed and which degree it alters are different facts.
|
|
38
|
+
// Reading `row.accidental` here drew nothing in that case, because the I
|
|
39
|
+
// is unaltered.
|
|
40
|
+
const sign = row.accidentalSign ?? row.accidental;
|
|
41
|
+
return { kind: "glyph", glyph: GLYPHS_BY_SET[glyphSet][sign] };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (mode === "heji") {
|
|
45
|
+
// A tuning is just-expressible when its departures from Pythagorean are
|
|
46
|
+
// whole syntonic commas (±21.5¢). An irrational tempering (meantone) leaves
|
|
47
|
+
// fractional deviations everywhere — heji cannot notate it. Detect that.
|
|
48
|
+
const devs = rows.map(fromPythagorean);
|
|
49
|
+
const SYNTONIC = 21.506;
|
|
50
|
+
const tempered = devs.some((d) => {
|
|
51
|
+
if (Math.abs(d) < 0.5)
|
|
52
|
+
return false; // on the pure-fifth chain
|
|
53
|
+
const commas = d / SYNTONIC;
|
|
54
|
+
return Math.abs(commas - Math.round(commas)) > 0.1; // not a whole comma
|
|
55
|
+
});
|
|
56
|
+
if (tempered) {
|
|
57
|
+
throw new Error("inscriptio: accidentals \"heji\" needs a just-expressible tuning; the " +
|
|
58
|
+
"current temperament tempers by fractional commas (e.g. meantone). Use " +
|
|
59
|
+
"accidentals: \"cents\" for tempered tunings.");
|
|
60
|
+
}
|
|
61
|
+
return rows.map((row) => {
|
|
62
|
+
const d = fromPythagorean(row);
|
|
63
|
+
if (Math.abs(d) < 0.5)
|
|
64
|
+
return null; // Pythagorean baseline — no arrow
|
|
65
|
+
const commas = Math.round(d / SYNTONIC);
|
|
66
|
+
if (commas === 0)
|
|
67
|
+
return null;
|
|
68
|
+
return { kind: "glyph", glyph: commas < 0 ? HEJI_COMMA_DOWN : HEJI_COMMA_UP };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// cents — signed deviation label, stated once per pitch class PER PHRASE.
|
|
72
|
+
// A deviating pitch is labelled at its first appearance in each phrase and
|
|
73
|
+
// rides silently through the phrase's repeats; the next phrase restates it
|
|
74
|
+
// (the reader's memory is the phrase, and chant has no measures to carry it).
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
return rows.map((row) => {
|
|
77
|
+
const value = centsBaseline === "et" ? row.offset : fromPythagorean(row);
|
|
78
|
+
if (Math.abs(value) < 0.5)
|
|
79
|
+
return null; // effectively in tune
|
|
80
|
+
const key = `${row.phraseIndex}:${row.pc}`;
|
|
81
|
+
if (seen.has(key))
|
|
82
|
+
return null; // already labelled in this phrase
|
|
83
|
+
seen.add(key);
|
|
84
|
+
const sign = value < 0 ? "−" : "+"; // real minus sign
|
|
85
|
+
return { kind: "cents", label: `${sign}${Math.abs(value).toFixed(1)}` };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=accidentals.js.map
|
|
@@ -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
|