Skip to content

Commit

Permalink
Merge pull request #245 from cuthbertLab/lyric_improvements
Browse files Browse the repository at this point in the history
More Lyric improvements: Alignment, X-Shift, etc.
  • Loading branch information
mscuthbert authored Mar 7, 2024
2 parents a364508 + c3de656 commit 7f1b4b6
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 74 deletions.
87 changes: 51 additions & 36 deletions src/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/
import {
Accidental as VFAccidental,
AnnotationHorizontalJustify as VFAnnotationHorizontalJustify,
Dot as VFDot,
StaveNote as VFStaveNote,
Stem as VFStem,
Expand Down Expand Up @@ -83,6 +84,12 @@ export interface VexflowNoteOptions {
clef?: clef.Clef,
}

export interface VexflowLyricOptions {
lyric_line?: number,
note?: GeneralNote,
vfn?: VFStaveNote,
}

export const default_vf_lyric_style = <Readonly<VFFontInfo>> {
family: 'Serif',
size: 12,
Expand Down Expand Up @@ -117,7 +124,15 @@ export class Lyric extends prebase.ProtoM21Object {
protected _identifier: string|number;
syllabic: string;
applyRaw: boolean;
style: Record<string, any>;
style: Record<string, any> = {
color: '',
fontFamily: 'Serif',
fontSize: 12,
fontWeight: '',
align: 'center',
xShift: 0,
// yShift: 0,
};

constructor(
text: string,
Expand All @@ -133,13 +148,6 @@ export class Lyric extends prebase.ProtoM21Object {
this.applyRaw = applyRaw ?? false;
this.setTextAndSyllabic(this.text, this.applyRaw);
this._identifier = identifier;
this.style = {
fillStyle: 'black',
strokeStyle: 'black',
fontFamily: 'Serif',
fontSize: 12,
fontWeight: '',
};
}

get identifier(): string|number {
Expand Down Expand Up @@ -212,6 +220,40 @@ export class Lyric extends prebase.ProtoM21Object {
}
return this;
}

vexflowLyric({lyric_line=-3}: VexflowLyricOptions = {}): VFLyricAnnotation {
const font: VFFontInfo = {...default_vf_lyric_style};
const style = this.style;
if (style.fontFamily) {
font.family = style.fontFamily;
}
if (style.fontSize) {
font.size = style.fontSize;
}
if (style.fontWeight) {
font.weight = style.fontWeight;
}
let text = this.text ?? '';
if (['middle', 'begin'].includes(this.syllabic)) {
text += ' ' + this.lyricConnector;
}

const annotation = new VFLyricAnnotation(text);
annotation.setFont(font);
if (style.align === 'left') {
annotation.setJustification(VFAnnotationHorizontalJustify.LEFT);
} else if (style.align === 'right') {
annotation.setJustification(VFAnnotationHorizontalJustify.RIGHT);
}
if (style.color) {
annotation.setFill(style.color);
}
if (style.xShift) {
annotation.setXShift(-1 * style.xShift); // VF measures backwards
}
annotation.setTextLine(5 - lyric_line + (this.number ?? 0) * 2);
return annotation;
}
}

/* Notes and rests etc... */
Expand Down Expand Up @@ -367,35 +409,8 @@ export class GeneralNote extends base.Music21Object {
*/
vexflowAddLyrics(vfn: VFStaveNote): void {
const lyric_line = this.activeSite?.renderOptions.lyricsLine ?? -3;
let level = 0;
for (const l of this.lyrics) {
let my_level: number;
if (l.number !== undefined) {
my_level = l.number - 1;
level = Math.max(level, my_level) + 1;
} else {
my_level = level;
level += 1;
}
const font: VFFontInfo = {...default_vf_lyric_style};
if (l.style.fontFamily) {
font.family = l.style.fontFamily;
}
if (l.style.fontSize) {
font.size = l.style.fontSize;
}
if (l.style.fontWeight) {
font.weight = l.style.fontWeight;
}
let text = l.text ?? '';
if (['middle', 'begin'].includes(l.syllabic)) {
text += ' ' + l.lyricConnector;
}

const annotation = new VFLyricAnnotation(text);
annotation.setFont(font);
annotation.setTextLine(5 - lyric_line + my_level*2);
vfn.addModifier(annotation, 0);
vfn.addModifier(l.vexflowLyric({lyric_line}), 0);
}
}

Expand Down
89 changes: 51 additions & 38 deletions src/vfShims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
log,
type ModifierContextState,
ModifierPosition,
type StemmableNote,
TextFormatter,
SVGContext,
} from 'vexflow';

// eslint-disable-next-line
Expand All @@ -38,55 +37,66 @@ Annotation.format = Annotation.format.bind(Annotation);

export class VFLyricAnnotation extends Annotation {
static DEBUG = false;
fill: string;

static format(annotations: VFLyricAnnotation[], state: ModifierContextState): boolean {
if (!annotations || annotations.length === 0) {
return false;
}
let leftWidth = 0;
let rightWidth = 0;
let maxLeftGlyphWidth = 0;
let maxRightGlyphWidth = 0;
for (let i = 0; i < annotations.length; ++i) {
const annotation = annotations[i];
const textFormatter = TextFormatter.create(annotation.textFont);

const note = annotation.checkAttachedNote();
const glyphWidth = note.getGlyphProps().getWidth();
const ctx = note.checkContext();
ctx.save();
ctx.setFont(annotation.fontInfo);
if (annotation.fill) {
ctx.setFillStyle(annotation.fill);
}
const textWidth = ctx.measureText(annotation.text).width;
ctx.restore();

// Get the text width from the font metrics.
const textWidth = textFormatter.getWidthForTextInPx(annotation.text);
if (annotation.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
maxLeftGlyphWidth = Math.max(glyphWidth, maxLeftGlyphWidth);
leftWidth = Math.max(leftWidth, textWidth) + Annotation.minAnnotationPadding;
} else if (annotation.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
maxRightGlyphWidth = Math.max(glyphWidth, maxRightGlyphWidth);
if (annotation.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
leftWidth = Math.max(leftWidth, textWidth + Annotation.minAnnotationPadding);
} else if (annotation.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
rightWidth = Math.max(rightWidth, textWidth);
} else {
leftWidth = Math.max(leftWidth, textWidth / 2) + Annotation.minAnnotationPadding;
leftWidth = Math.max(leftWidth, textWidth / 2 + Annotation.minAnnotationPadding);
rightWidth = Math.max(rightWidth, textWidth / 2);
maxLeftGlyphWidth = Math.max(glyphWidth / 2, maxLeftGlyphWidth);
maxRightGlyphWidth = Math.max(glyphWidth / 2, maxRightGlyphWidth);
}
}
const rightOverlap = Math.min(
Math.max(rightWidth - maxRightGlyphWidth, 0),
Math.max(rightWidth - state.right_shift, 0)
);
const leftOverlap = Math.min(
Math.max(leftWidth - maxLeftGlyphWidth, 0),
Math.max(leftWidth - state.left_shift, 0)
);
const rightOverlap = Math.max(rightWidth - state.right_shift, 0);
const leftOverlap = Math.max(leftWidth - state.left_shift, 0);
state.left_shift += leftOverlap;
state.right_shift += rightOverlap;
return true;
}

getFill(): string {
return this.fill;
}

setFill(color: string) {
this.fill = color;
}

/** Render text below the note at the given staff line */
draw(): void {
const ctx = this.checkContext();
const ctx = <SVGContext> this.checkContext();
if (ctx.svg === undefined) {
throw new Error('Can only add lyrics to SVG Context not Canvas');
}
const note = this.checkAttachedNote();
const textFormatter = TextFormatter.create(this.textFont);
const start_x = note.getModifierStartXY(ModifierPosition.ABOVE, this.index).x;
let x = note.getModifierStartXY(ModifierPosition.ABOVE, this.index).x;
if (this.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
x -= note.getGlyphWidth() / 2;
}
if (this.getXShift()) {
x += this.getXShift();
}

this.setRendered();

Expand All @@ -96,27 +106,30 @@ export class VFLyricAnnotation extends Annotation {
// still need to save context state just before this, since we will be
// changing ctx parameters below.
this.applyStyle();
ctx.openGroup('annotation', this.getAttribute('id'));
const g: SVGGElement = ctx.openGroup('lyricannotation', this.getAttribute('id'));
ctx.setFont(this.textFont);
if (this.fill) {
ctx.setFillStyle(this.fill);
}

const text_width = textFormatter.getWidthForTextInPx(this.text);
let x: number;
const stave = note.checkStave();
const y = stave.getYForLine(this.text_line);

L('Rendering annotation: ', this.text, x, y);
ctx.fillText(this.text, x, y);
const svg_text = g.lastElementChild as SVGTextElement;
if (this.horizontalJustification === AnnotationHorizontalJustify.LEFT) {
x = start_x;
// left is default;
} else if (this.horizontalJustification === AnnotationHorizontalJustify.RIGHT) {
x = start_x - text_width;
svg_text.setAttribute('text-anchor', 'right');
} else if (this.horizontalJustification === AnnotationHorizontalJustify.CENTER) {
x = start_x - text_width / 2;
svg_text.setAttribute('text-anchor', 'middle');
} /* CENTER_STEM */ else {
x = (note as StemmableNote).getStemX() - text_width / 2;
// TODO: this should be slightly different.
// x = (note as StemmableNote).getStemX() - text_width / 2;
svg_text.setAttribute('text-anchor', 'middle');
}

const stave = note.checkStave();
const y = stave.getYForLine(this.text_line);

L('Rendering annotation: ', this.text, x, y);
ctx.fillText(this.text, x, y);
ctx.closeGroup();
this.restoreStyle();
ctx.restore();
Expand Down

0 comments on commit 7f1b4b6

Please sign in to comment.