Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render lilypond33c #201

Merged
merged 5 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/rendering/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,11 @@ export class Score {
beam.vexflow.beam?.setContext(vfContext).draw();
});

// Draw vexflow.StaveTie elements for slurs.
// Draw vexflow.Curve elements for slurs.
spannersRendering.slurs
.flatMap((slur) => slur.vexflow.tie)
.forEach((vfStaveTie) => {
vfStaveTie.setContext(vfContext).draw();
.flatMap((slur) => slur.vexflow.curve)
.forEach((vfCurve) => {
vfCurve?.setContext(vfContext).draw();
});

// Draw vexflow.StaveTie elements for ties.
Expand Down
127 changes: 104 additions & 23 deletions src/rendering/slur.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { SpannerMap } from './spannermap';
import { SpannerData } from './types';
import { Address } from './address';

const SLUR_PADDING_PER_NOTE = 20;

/** The result of rendering a slur. */
export type SlurRendering = {
type: 'slur';
vexflow: {
tie: vexflow.StaveTie;
curve: vexflow.Curve | null;
};
};

Expand All @@ -22,6 +24,9 @@ export type SlurFragment = {
type: SlurFragmentType;
number: number;
address: Address;
musicXML: {
slur: musicxml.Slur;
};
vexflow: {
note: vexflow.Note;
keyIndex: number;
Expand All @@ -31,6 +36,8 @@ export type SlurFragment = {
/** The container for slurs. */
type SlurContainer = SpannerMap<number, Slur>;

type CurveOpeningDirection = 'up' | 'down' | 'unknown';

/** Represents a curved line that connects two or more different notes of varying pitch to indicate that they should be
* played legato.
*/
Expand Down Expand Up @@ -61,6 +68,7 @@ export class Slur {
type: slurType,
number: slur.getNumber(),
address: data.address,
musicXML: { slur },
vexflow: {
note: data.vexflow.staveNote,
keyIndex: data.keyIndex,
Expand Down Expand Up @@ -95,33 +103,35 @@ export class Slur {
}
}

getExtraMeasureFragmentWidth(address: Address<'measurefragment'>): number {
return (
this.fragments.filter((fragment) => fragment.address.isMemberOf('measurefragment', address)).length *
SLUR_PADDING_PER_NOTE
);
}

/** Renders the slur. */
render(): SlurRendering {
const vfTieNotes: vexflow.TieNotes = {};
const vfStartNote = this.fragments.find((fragment) => fragment.type === 'start')?.vexflow.note;
const vfStopNote = this.fragments.find((fragment) => fragment.type === 'stop')?.vexflow.note;

for (let index = 0; index < this.fragments.length; index++) {
const fragment = this.fragments[index];
const isFirst = index === 0;
const isLast = index === this.fragments.length - 1;

// Iterating is not necessary, but it is cleaner than dealing with nulls.
if (isFirst) {
vfTieNotes.firstNote = fragment.vexflow.note;
vfTieNotes.firstIndexes = [fragment.vexflow.keyIndex];
}
if (isLast) {
vfTieNotes.lastNote = fragment.vexflow.note;
vfTieNotes.lastIndexes = [fragment.vexflow.keyIndex];
}
if (!vfStartNote && !vfStopNote) {
return {
type: 'slur',
vexflow: { curve: null },
};
}

const vfSlurDirection = this.getVfSlurDirection();
const vfTie = new vexflow.StaveTie(vfTieNotes).setDirection(vfSlurDirection);
const vfCurveOptions = this.getVfCurveOptions({ vexflow: { startNote: vfStartNote, stopNote: vfStopNote } });

// Partial curves are allowed, but the types disallow it:
// https://github.com/0xfe/vexflow/blob/8ddc8fa1a6d304a879e73830919fa17f3a9bdef4/src/curve.ts#L87
const vfCurve = new vexflow.Curve(vfStartNote as any, vfStopNote as any, vfCurveOptions);

return {
type: 'slur',
vexflow: {
tie: vfTie,
curve: vfCurve,
},
};
}
Expand All @@ -130,12 +140,79 @@ export class Slur {
return util.last(this.fragments)!;
}

private getVfSlurDirection(): number {
const slurPlacement = this.getSlurPlacement();
return conversions.fromAboveBelowToVexflowSlurDirection(slurPlacement);
private getVfCurveOptions(opts: {
vexflow: { startNote: vexflow.Note | undefined; stopNote: vexflow.Note | undefined };
}): vexflow.CurveOptions {
const placement = this.getSlurPlacement();

const startStem = this.getStem(opts.vexflow.startNote);
const stopStem = this.getStem(opts.vexflow.stopNote);

const startPosition = this.getVfCurvePosition(placement, startStem);
const stopPosition = this.getVfCurvePosition(placement, stopStem);

const openingDirection = this.getCurveOpeningDirection(opts.vexflow.startNote, opts.vexflow.stopNote);
const invert =
(openingDirection === 'up' && placement === 'above') || (openingDirection === 'down' && placement === 'below');

return {
position: startPosition,
positionEnd: stopPosition,
invert,
};
}

private getVfCurvePosition(
placement: musicxml.AboveBelow,
stem: musicxml.Stem | undefined
): vexflow.CurvePosition | undefined {
if (placement === 'above' && stem === 'up') {
return vexflow.CurvePosition.NEAR_TOP;
}
if (placement === 'above' && stem === 'down') {
return vexflow.CurvePosition.NEAR_HEAD;
}
if (placement === 'below' && stem === 'up') {
return vexflow.CurvePosition.NEAR_HEAD;
}
if (placement === 'below' && stem === 'down') {
return vexflow.CurvePosition.NEAR_TOP;
}
return undefined;
}

private getCurveOpeningDirection(
startNote: vexflow.Note | undefined,
stopNote: vexflow.Note | undefined
): CurveOpeningDirection {
let note: vexflow.Note;

if (startNote && stopNote) {
note = stopNote;
} else if (startNote) {
note = startNote;
} else if (stopNote) {
note = stopNote;
} else {
return 'unknown';
}

switch (this.getStem(note)) {
case 'up':
return 'up';
case 'down':
return 'down';
default:
return 'unknown';
}
}

private getSlurPlacement(): musicxml.AboveBelow {
const placement = this.fragments[0].musicXML.slur.getPlacement();
if (placement) {
return placement;
}

const vfNote = util.first(this.fragments)?.vexflow.note;
if (!vfNote) {
return 'above';
Expand Down Expand Up @@ -166,7 +243,11 @@ export class Slur {
}
}

private getStem(vfNote: vexflow.Note): musicxml.Stem {
private getStem(vfNote: vexflow.Note | undefined): musicxml.Stem | undefined {
if (!vfNote) {
return undefined;
}

// Calling getStemDirection will throw if there is no stem.
// https://github.com/0xfe/vexflow/blob/7e7eb97bf1580a31171302b3bd8165f057b692ba/src/stemmablenote.ts#L118
try {
Expand Down
8 changes: 7 additions & 1 deletion src/rendering/spanners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ export class Spanners {

/** Returns the additional padding needed to accommodate some spanners. */
getExtraMeasureFragmentWidth(address: Address<'measurefragment'>): number {
return util.sum(this.tuplets.values().map((tuplet) => tuplet.getExtraMeasureFragmentWidth(address)));
const tupletExtraWidth = util.sum(
this.tuplets.values().map((tuplet) => tuplet.getExtraMeasureFragmentWidth(address))
);

const slurExtraWidth = util.sum(this.slurs.values().map((slur) => slur.getExtraMeasureFragmentWidth(address)));

return tupletExtraWidth + slurExtraWidth;
}

/** Extracts and processes all the spanners within the given data. */
Expand Down
Binary file modified tests/integration/__image_snapshots__/33a-Spanners_900px.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/integration/lilypond.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ describe('lilypond', () => {
// { filename: '32d-Arpeggio.musicxml', width: 900 },
{ filename: '33a-Spanners.musicxml', width: 900 },
{ filename: '33b-Spanners-Tie.musicxml', width: 900 },
// { filename: '33c-Spanners-Slurs.musicxml', width: 900 },
{ filename: '33c-Spanners-Slurs.musicxml', width: 900 },
// { filename: '33d-Spanners-OctaveShifts.musicxml', width: 900 },
// { filename: '33e-Spanners-OctaveShifts-InvalidSize.musicxml', width: 900 },
// { filename: '33f-Trill-EndingOnGraceNote.musicxml', width: 900 },
Expand Down