Skip to content

Commit 0e06103

Browse files
committed
fix(accidental): extra space in 2nds w/ stems down
1 parent 625ab61 commit 0e06103

File tree

2 files changed

+192
-50
lines changed

2 files changed

+192
-50
lines changed

src/accidental.ts

Lines changed: 128 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@ import { Category, isAccidental, isGraceNote, isGraceNoteGroup, isStaveNote } fr
1515
import { defined, log } from './util';
1616
import { Voice } from './voice';
1717

18-
export type Line = {
18+
type StaveLineAccidentalLayoutMetrics = {
1919
column: number;
2020
line: number;
21+
/**
22+
* A flat line needs more clearance above than below. This is
23+
* set to true if the accidental is either a flat or double flat.
24+
*/
2125
flatLine: boolean;
26+
/**
27+
* Double sharps need less clearance above and below than other
28+
* accidentals.
29+
*/
2230
dblSharpLine: boolean;
2331
numAcc: number;
2432
width: number;
@@ -74,95 +82,147 @@ export class Accidental extends Modifier {
7482
const additionalPadding = musicFont.lookupMetric('accidental.leftPadding'); // padding to the left of all accidentals
7583

7684
// A type used just in this formatting function.
77-
type AccidentalListItem = {
85+
type AccidentalLinePositionsAndXSpaceNeeds = {
7886
y?: number;
7987
line: number;
80-
shift: number;
88+
/**
89+
* The amount by which the accidental requests notes be shifted to the right
90+
* to accomodate its presence.
91+
*/
92+
extraXSpaceNeeded: number;
8193
acc: Accidental;
82-
lineSpace?: number;
94+
spacingBetweenStaveLines?: number;
8395
};
8496

85-
const accList: AccidentalListItem[] = [];
97+
const accidentalLinePositionsAndSpaceNeeds: AccidentalLinePositionsAndXSpaceNeeds[] = [];
8698
let prevNote = undefined;
87-
let shiftL = 0;
99+
let extraXSpaceNeededForLeftDisplacedNotehead = 0;
88100

89101
// First determine the accidentals' Y positions from the note.keys
90102
for (let i = 0; i < accidentals.length; ++i) {
91103
const acc: Accidental = accidentals[i];
92104

93105
const note = acc.getNote();
106+
const stemDirection = note.getStemDirection();
94107
const stave = note.getStave();
95108
const index = acc.checkIndex();
96109
const props = note.getKeyProps()[index];
110+
111+
let headDisplacementXShift = 0;
97112
if (note !== prevNote) {
98113
// Iterate through all notes to get the displaced pixels
99114
for (let n = 0; n < note.keys.length; ++n) {
100-
shiftL = Math.max(note.getLeftDisplacedHeadPx() - note.getXShift(), shiftL);
115+
const priorShiftL = extraXSpaceNeededForLeftDisplacedNotehead;
116+
// The extra space we need for the accidental to the left is the amount
117+
// that the left-displaced notehead has been shifted left, less any
118+
// extra space that has already been added via the note's xShift.
119+
const leftDisplacedNoteheadShift = note.getLeftDisplacedHeadPx() - note.getXShift();
120+
// If the current extra left-space needed (shiftL) isn't as big as that,
121+
// then we need to set it. But, this will give the accidental ownership of
122+
// that x-space, and later when formatting the note, its x_begin would have
123+
// twice the getLeftDisplacedHeadPx included in it. So: we need to remove
124+
// it from the note's x shift.
125+
// if (leftDisplacedNoteheadShift > extraXSpaceNeededForLeftDisplacedNotehead) {
126+
// extraXSpaceNeededForLeftDisplacedNotehead = leftDisplacedNoteheadShift;
127+
// note.setLeftDisplacedHeadPx(-note.getXShift());
128+
// }
129+
extraXSpaceNeededForLeftDisplacedNotehead = Math.max(
130+
note.getLeftDisplacedHeadPx() - note.getXShift(),
131+
extraXSpaceNeededForLeftDisplacedNotehead
132+
);
133+
headDisplacementXShift = note.getLeftDisplacedHeadPx();
134+
135+
// console.log({
136+
// getLeftDisplacedHeadPx: note.getLeftDisplacedHeadPx(),
137+
// getXShift: note.getXShift(),
138+
// getLeftDisplacedHeadPx_minus_getXShift: note.getLeftDisplacedHeadPx() - note.getXShift(),
139+
// shiftL: extraXSpaceNeededForLeftDisplacedNotehead,
140+
// priorShiftL,
141+
// });
101142
}
102143
prevNote = note;
103144
}
104145
if (stave) {
105146
const lineSpace = stave.getSpacingBetweenLines();
106147
const y = stave.getYForLine(props.line);
107148
const accLine = Math.round((y / lineSpace) * 2) / 2;
108-
accList.push({ y, line: accLine, shift: shiftL, acc, lineSpace });
149+
accidentalLinePositionsAndSpaceNeeds.push({
150+
y,
151+
line: accLine,
152+
extraXSpaceNeeded: extraXSpaceNeededForLeftDisplacedNotehead,
153+
acc,
154+
spacingBetweenStaveLines: lineSpace,
155+
});
109156
} else {
110-
accList.push({ line: props.line, shift: shiftL, acc });
157+
accidentalLinePositionsAndSpaceNeeds.push({
158+
line: props.line,
159+
extraXSpaceNeeded: extraXSpaceNeededForLeftDisplacedNotehead,
160+
acc,
161+
});
111162
}
112163
}
113164

114165
// Sort accidentals by line number.
115-
accList.sort((a, b) => b.line - a.line);
166+
accidentalLinePositionsAndSpaceNeeds.sort((a, b) => b.line - a.line);
116167

117168
// FIXME: Confusing name. Each object in this array has a property called `line`.
118169
// So if this is a list of lines, you end up with: `line.line` which is very awkward.
119-
const lineList: Line[] = [];
170+
const staveLineAccidentalLayoutMetrics: StaveLineAccidentalLayoutMetrics[] = [];
120171

121172
// amount by which all accidentals must be shifted right or left for
122173
// stem flipping, notehead shifting concerns.
123-
let accShift = 0;
124-
let previousLine = undefined;
174+
let maxExtraXSpaceNeeded = 0;
125175

126-
// Create an array of unique line numbers (lineList) from accList
127-
for (let i = 0; i < accList.length; i++) {
128-
const acc = accList[i];
176+
// Create an array of unique line numbers (staveLineAccidentalLayoutMetrics)
177+
// from accidentalLinePositionsAndSpaceNeeds
178+
for (let i = 0; i < accidentalLinePositionsAndSpaceNeeds.length; i++) {
179+
const accidentalLinePositionAndSpaceNeeds = accidentalLinePositionsAndSpaceNeeds[i];
129180

130-
// if this is the first line, or a new line, add a lineList
131-
if (previousLine === undefined || previousLine !== acc.line) {
132-
lineList.push({
133-
line: acc.line,
181+
const priorLineMetric = staveLineAccidentalLayoutMetrics[staveLineAccidentalLayoutMetrics.length - 1];
182+
let currentLineMetric: StaveLineAccidentalLayoutMetrics;
183+
184+
// if this is the first line, or a new line, add a staveLineAccidentalLayoutMetric
185+
if (!priorLineMetric || priorLineMetric?.line !== accidentalLinePositionAndSpaceNeeds.line) {
186+
currentLineMetric = {
187+
line: accidentalLinePositionAndSpaceNeeds.line,
134188
flatLine: true,
135189
dblSharpLine: true,
136190
numAcc: 0,
137191
width: 0,
138192
column: 0,
139-
});
193+
};
194+
staveLineAccidentalLayoutMetrics.push(currentLineMetric);
195+
} else {
196+
currentLineMetric = priorLineMetric;
140197
}
198+
141199
// if this accidental is not a flat, the accidental needs 3.0 lines lower
142200
// clearance instead of 2.5 lines for b or bb.
143201
// FIXME: Naming could use work. acc.acc is very awkward
144-
if (acc.acc.type !== 'b' && acc.acc.type !== 'bb') {
145-
lineList[lineList.length - 1].flatLine = false;
202+
if (
203+
accidentalLinePositionAndSpaceNeeds.acc.type !== 'b' &&
204+
accidentalLinePositionAndSpaceNeeds.acc.type !== 'bb'
205+
) {
206+
currentLineMetric.flatLine = false;
146207
}
147208

148209
// if this accidental is not a double sharp, the accidental needs 3.0 lines above
149-
if (acc.acc.type !== '##') {
150-
lineList[lineList.length - 1].dblSharpLine = false;
210+
if (accidentalLinePositionAndSpaceNeeds.acc.type !== '##') {
211+
currentLineMetric.dblSharpLine = false;
151212
}
152213

153214
// Track how many accidentals are on this line:
154-
lineList[lineList.length - 1].numAcc++;
215+
currentLineMetric.numAcc++;
155216

156217
// Track the total x_offset needed for this line which will be needed
157218
// for formatting lines w/ multiple accidentals:
158219

159220
// width = accidental width + universal spacing between accidentals
160-
lineList[lineList.length - 1].width += acc.acc.getWidth() + accidentalSpacing;
161-
162-
// if this accShift is larger, use it to keep first column accidentals in the same line
163-
accShift = acc.shift > accShift ? acc.shift : accShift;
221+
currentLineMetric.width += accidentalLinePositionAndSpaceNeeds.acc.getWidth() + accidentalSpacing;
164222

165-
previousLine = acc.line;
223+
// if this extraXSpaceNeeded is the largest so far, use it as the starting point for
224+
// all accidental columns.
225+
maxExtraXSpaceNeeded = Math.max(accidentalLinePositionAndSpaceNeeds.extraXSpaceNeeded, maxExtraXSpaceNeeded);
166226
}
167227

168228
// ### Place Accidentals in Columns
@@ -186,14 +246,19 @@ export class Accidental extends Modifier {
186246
let totalColumns = 0;
187247

188248
// establish the boundaries for a group of notes with clashing accidentals:
189-
for (let i = 0; i < lineList.length; i++) {
249+
for (let i = 0; i < staveLineAccidentalLayoutMetrics.length; i++) {
190250
let noFurtherConflicts = false;
191251
const groupStart = i;
192252
let groupEnd = i;
193253

194-
while (groupEnd + 1 < lineList.length && !noFurtherConflicts) {
254+
while (groupEnd + 1 < staveLineAccidentalLayoutMetrics.length && !noFurtherConflicts) {
195255
// if this note conflicts with the next:
196-
if (this.checkCollision(lineList[groupEnd], lineList[groupEnd + 1])) {
256+
if (
257+
this.checkCollision(
258+
staveLineAccidentalLayoutMetrics[groupEnd],
259+
staveLineAccidentalLayoutMetrics[groupEnd + 1]
260+
)
261+
) {
197262
// include the next note in the group:
198263
groupEnd++;
199264
} else {
@@ -202,7 +267,7 @@ export class Accidental extends Modifier {
202267
}
203268

204269
// Gets an a line from the `lineList`, relative to the current group
205-
const getGroupLine = (index: number) => lineList[groupStart + index];
270+
const getGroupLine = (index: number) => staveLineAccidentalLayoutMetrics[groupStart + index];
206271
const getGroupLines = (indexes: number[]) => indexes.map(getGroupLine);
207272
const lineDifference = (indexA: number, indexB: number) => {
208273
const [a, b] = getGroupLines([indexA, indexB]).map((item) => item.line);
@@ -216,7 +281,12 @@ export class Accidental extends Modifier {
216281
const groupLength = groupEnd - groupStart + 1;
217282

218283
// Set the accidental column for each line of the group
219-
let endCase = this.checkCollision(lineList[groupStart], lineList[groupEnd]) ? 'a' : 'b';
284+
let endCase = this.checkCollision(
285+
staveLineAccidentalLayoutMetrics[groupStart],
286+
staveLineAccidentalLayoutMetrics[groupEnd]
287+
)
288+
? 'a'
289+
: 'b';
220290

221291
switch (groupLength) {
222292
case 3:
@@ -259,8 +329,13 @@ export class Accidental extends Modifier {
259329
let collisionDetected = true;
260330
while (collisionDetected === true) {
261331
collisionDetected = false;
262-
for (let line = 0; line + patternLength < lineList.length; line++) {
263-
if (this.checkCollision(lineList[line], lineList[line + patternLength])) {
332+
for (let line = 0; line + patternLength < staveLineAccidentalLayoutMetrics.length; line++) {
333+
if (
334+
this.checkCollision(
335+
staveLineAccidentalLayoutMetrics[line],
336+
staveLineAccidentalLayoutMetrics[line + patternLength]
337+
)
338+
) {
264339
collisionDetected = true;
265340
patternLength++;
266341
break;
@@ -270,15 +345,15 @@ export class Accidental extends Modifier {
270345
// Then, assign a column to each line of accidentals
271346
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
272347
column = ((groupMember - i) % patternLength) + 1;
273-
lineList[groupMember].column = column;
348+
staveLineAccidentalLayoutMetrics[groupMember].column = column;
274349
totalColumns = totalColumns > column ? totalColumns : column;
275350
}
276351
} else {
277352
// If the group contains fewer than seven members, use the layouts from
278353
// the Tables.accidentalColumnsTable (See: tables.ts).
279354
for (groupMember = i; groupMember <= groupEnd; groupMember++) {
280355
column = Tables.accidentalColumnsTable[groupLength][endCase][groupMember - i];
281-
lineList[groupMember].column = column;
356+
staveLineAccidentalLayoutMetrics[groupMember].column = column;
282357
totalColumns = totalColumns > column ? totalColumns : column;
283358
}
284359
}
@@ -308,12 +383,14 @@ export class Accidental extends Modifier {
308383
columnXOffsets[i] = 0;
309384
}
310385

311-
columnWidths[0] = accShift + leftShift;
312-
columnXOffsets[0] = accShift + leftShift;
386+
// columnWidths[0] = maxExtraXSpaceNeeded + leftShift;
387+
// columnXOffsets[0] = maxExtraXSpaceNeeded + leftShift;
388+
columnWidths[0] = leftShift + maxExtraXSpaceNeeded;
389+
columnXOffsets[0] = leftShift;
313390

314391
// Fill columnWidths with widest needed x-space;
315392
// this is what keeps the columns parallel.
316-
lineList.forEach((line) => {
393+
staveLineAccidentalLayoutMetrics.forEach((line) => {
317394
if (line.width > columnWidths[line.column]) columnWidths[line.column] = line.width;
318395
});
319396

@@ -325,26 +402,27 @@ export class Accidental extends Modifier {
325402
const totalShift = columnXOffsets[columnXOffsets.length - 1];
326403
// Set the xShift for each accidental according to column offsets:
327404
let accCount = 0;
328-
lineList.forEach((line) => {
405+
staveLineAccidentalLayoutMetrics.forEach((line) => {
329406
let lineWidth = 0;
330407
const lastAccOnLine = accCount + line.numAcc;
331408
// handle all of the accidentals on a given line:
332409
for (accCount; accCount < lastAccOnLine; accCount++) {
333-
const xShift = columnXOffsets[line.column - 1] + lineWidth;
334-
accList[accCount].acc.setXShift(xShift);
410+
const xShift = columnXOffsets[line.column - 1] + lineWidth + maxExtraXSpaceNeeded;
411+
accidentalLinePositionsAndSpaceNeeds[accCount].acc.setXShift(xShift);
335412
// keep track of the width of accidentals we've added so far, so that when
336413
// we loop, we add space for them.
337-
lineWidth += accList[accCount].acc.getWidth() + accidentalSpacing;
414+
lineWidth += accidentalLinePositionsAndSpaceNeeds[accCount].acc.getWidth() + accidentalSpacing;
415+
// console.log('Line, accCount, shift: ', line.line, accCount, xShift);
338416
L('Line, accCount, shift: ', line.line, accCount, xShift);
339417
}
340418
});
341-
419+
// console.log({ totalShift, columnXOffsets, additionalPadding });
342420
// update the overall layout with the full width of the accidental shapes:
343-
state.left_shift += totalShift + additionalPadding;
421+
state.left_shift = totalShift + additionalPadding;
344422
}
345423

346424
/** Helper function to determine whether two lines of accidentals collide vertically */
347-
static checkCollision(line1: Line, line2: Line): boolean {
425+
static checkCollision(line1: StaveLineAccidentalLayoutMetrics, line2: StaveLineAccidentalLayoutMetrics): boolean {
348426
let clearance = line2.line - line1.line;
349427
let clearanceRequired = 3;
350428
// But less clearance is required for certain accidentals: b, bb and ##.

test.html

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!DOCTYPE >
2+
<!--
3+
This example is available at:
4+
https://jsfiddle.net/wjm3t5su/ -->
5+
<html>
6+
<style>
7+
body {
8+
font-family: Arial, 'sans-serif';
9+
}
10+
</style>
11+
<body>
12+
<div id="output"></div>
13+
<div style="height: 1000px"></div>
14+
<script src="./build/cjs/vexflow-debug.js"></script>
15+
<script>
16+
const { Factory, EasyScore, System } = Vex.Flow;
17+
18+
[
19+
'(A4 B#4)',
20+
'(B4 C#5)',
21+
22+
// 'C5 D#5',
23+
// 'D5 E#5',
24+
// 'E5 F#5',
25+
// 'E#4 F4',
26+
// 'F#4 G4',
27+
// 'G#4 A4',
28+
// 'A#4 B4',
29+
// 'B#4 C5',
30+
// 'C#5 D5',
31+
// 'D#5 E5',
32+
// 'E#5 F5',
33+
// 'E#4 F#4',
34+
// 'F#4 G#4',
35+
// 'G#4 A#4',
36+
// 'A#4 B#4',
37+
// 'B#4 C#5',
38+
// 'C#5 D#5',
39+
// 'D#5 E#5',
40+
// 'E#5 F#5',
41+
].forEach((noteNames) => {
42+
console.log(`%cNotes: ${noteNames}`, 'color: #990000; background-color: #AAAAAA; padding: 1em 1em;');
43+
const id = crypto.randomUUID();
44+
const div = document.createElement('div');
45+
div.setAttribute('id', id);
46+
document.querySelector('#output').appendChild(div);
47+
const vf = new Factory({
48+
renderer: { elementId: id, width: 500, height: 200 },
49+
});
50+
51+
const score = vf.EasyScore().set({ time: '2/4' });
52+
const system = vf.System();
53+
54+
system
55+
.addStave({
56+
voices: [score.voice(score.notes(`${noteNames}/h`))],
57+
})
58+
.addClef('treble')
59+
.addTimeSignature('2/4');
60+
vf.draw();
61+
});
62+
</script>
63+
</body>
64+
</html>

0 commit comments

Comments
 (0)