vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
304 lines (260 loc) • 8.47 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// MIT License
import { Element } from './element';
import { FontInfo } from './font';
import { Modifier } from './modifier';
import { ModifierContextState } from './modifiercontext';
import { TextFormatter } from './textformatter';
import { Category, isTabNote } from './typeguard';
import { RuntimeError } from './util';
export interface BendPhrase {
x?: number;
type: number;
text: string;
width?: number;
draw_width?: number;
}
/** Bend implements tablature bends. */
export class Bend extends Modifier {
static get CATEGORY(): string {
return Category.Bend;
}
static get UP(): number {
return 0;
}
static get DOWN(): number {
return 1;
}
/** Default text font. */
static TEXT_FONT: Required<FontInfo> = { ...Element.TEXT_FONT };
// Arrange bends in `ModifierContext`
static format(bends: Bend[], state: ModifierContextState): boolean {
if (!bends || bends.length === 0) return false;
let last_width = 0;
// Format Bends
for (let i = 0; i < bends.length; ++i) {
const bend = bends[i];
const note = bend.checkAttachedNote();
if (isTabNote(note)) {
const stringPos = note.leastString() - 1;
if (state.top_text_line < stringPos) {
state.top_text_line = stringPos;
}
}
bend.setXShift(last_width);
last_width = bend.getWidth();
bend.setTextLine(state.top_text_line);
}
state.right_shift += last_width;
state.top_text_line += 1;
return true;
}
protected text: string;
protected tap: string;
protected release: boolean;
protected phrase: BendPhrase[];
public render_options: {
line_width: number;
release_width: number;
bend_width: number;
line_style: string;
};
/**
* Example of a phrase:
* ```
* [{
* type: UP,
* text: "whole"
* width: 8;
* },
* {
* type: DOWN,
* text: "whole"
* width: 8;
* },
* {
* type: UP,
* text: "half"
* width: 8;
* },
* {
* type: UP,
* text: "whole"
* width: 8;
* },
* {
* type: DOWN,
* text: "1 1/2"
* width: 8;
* }]
* ```
* @param text text for bend ("Full", "Half", etc.) (DEPRECATED)
* @param release if true, render a release. (DEPRECATED)
* @param phrase if set, ignore "text" and "release", and use the more sophisticated phrase specified
*/
constructor(text: string, release: boolean = false, phrase?: BendPhrase[]) {
super();
this.text = text;
this.x_shift = 0;
this.release = release;
this.tap = '';
this.resetFont();
this.render_options = {
line_width: 1.5,
line_style: '#777777',
bend_width: 8,
release_width: 8,
};
if (phrase) {
this.phrase = phrase;
} else {
// Backward compatibility
this.phrase = [{ type: Bend.UP, text: this.text }];
if (this.release) this.phrase.push({ type: Bend.DOWN, text: '' });
}
this.updateWidth();
}
/** Set horizontal shift in pixels. */
setXShift(value: number): this {
this.x_shift = value;
this.updateWidth();
return this;
}
setTap(value: string): this {
this.tap = value;
return this;
}
/** Get text provided in the constructor. */
getText(): string {
return this.text;
}
getTextHeight(): number {
const textFormatter = TextFormatter.create(this.textFont);
return textFormatter.maxHeight;
}
/** Recalculate width. */
protected updateWidth(): this {
const textFormatter = TextFormatter.create(this.textFont);
const measureText = (text: string) => {
return textFormatter.getWidthForTextInPx(text);
};
let totalWidth = 0;
for (let i = 0; i < this.phrase.length; ++i) {
const bend = this.phrase[i];
if (bend.width !== undefined) {
totalWidth += bend.width;
} else {
const additional_width =
bend.type === Bend.UP ? this.render_options.bend_width : this.render_options.release_width;
bend.width = Math.max(additional_width, measureText(bend.text)) + 3;
bend.draw_width = bend.width / 2;
totalWidth += bend.width;
}
}
this.setWidth(totalWidth + this.x_shift);
return this;
}
/** Draw the bend on the rendering context. */
draw(): void {
const ctx = this.checkContext();
const note = this.checkAttachedNote();
this.setRendered();
const start = note.getModifierStartXY(Modifier.Position.RIGHT, this.index);
start.x += 3;
start.y += 0.5;
const x_shift = this.x_shift;
const stave = note.checkStave();
const spacing = stave.getSpacingBetweenLines();
const lowestY = note.getYs().reduce((a, b) => (a < b ? a : b));
// this.text_line is relative to top string in the group.
const bend_height = start.y - ((this.text_line + 1) * spacing + start.y - lowestY) + 3;
const annotation_y = start.y - ((this.text_line + 1) * spacing + start.y - lowestY) - 1;
const renderBend = (x: number, y: number, width: number, height: number) => {
const cp_x = x + width;
const cp_y = y;
ctx.save();
ctx.beginPath();
ctx.setLineWidth(this.render_options.line_width);
ctx.setStrokeStyle(this.render_options.line_style);
ctx.setFillStyle(this.render_options.line_style);
ctx.moveTo(x, y);
ctx.quadraticCurveTo(cp_x, cp_y, x + width, height);
ctx.stroke();
ctx.restore();
};
const renderRelease = (x: number, y: number, width: number, height: number) => {
ctx.save();
ctx.beginPath();
ctx.setLineWidth(this.render_options.line_width);
ctx.setStrokeStyle(this.render_options.line_style);
ctx.setFillStyle(this.render_options.line_style);
ctx.moveTo(x, height);
ctx.quadraticCurveTo(x + width, height, x + width, y);
ctx.stroke();
ctx.restore();
};
const renderArrowHead = (x: number, y: number, direction: number) => {
const width = 4;
const yBase = y + width * direction;
ctx.beginPath();
ctx.moveTo(x, y); // tip of the arrow
ctx.lineTo(x - width, yBase);
ctx.lineTo(x + width, yBase);
ctx.closePath();
ctx.fill();
};
const renderText = (x: number, text: string) => {
ctx.save();
ctx.setFont(this.textFont);
const render_x = x - ctx.measureText(text).width / 2;
ctx.fillText(text, render_x, annotation_y);
ctx.restore();
};
let last_bend = undefined;
let last_bend_draw_width = 0;
let last_drawn_width = 0;
if (this.tap?.length) {
const tapStart = note.getModifierStartXY(Modifier.Position.CENTER, this.index);
renderText(tapStart.x, this.tap);
}
for (let i = 0; i < this.phrase.length; ++i) {
const bend = this.phrase[i];
if (!bend.draw_width) bend.draw_width = 0;
if (i === 0) bend.draw_width += x_shift;
last_drawn_width = bend.draw_width + last_bend_draw_width - (i === 1 ? x_shift : 0);
if (bend.type === Bend.UP) {
if (last_bend && last_bend.type === Bend.UP) {
renderArrowHead(start.x, bend_height, +1);
}
renderBend(start.x, start.y, last_drawn_width, bend_height);
}
if (bend.type === Bend.DOWN) {
if (last_bend && last_bend.type === Bend.UP) {
renderRelease(start.x, start.y, last_drawn_width, bend_height);
}
if (last_bend && last_bend.type === Bend.DOWN) {
renderArrowHead(start.x, start.y, -1);
renderRelease(start.x, start.y, last_drawn_width, bend_height);
}
if (!last_bend) {
last_drawn_width = bend.draw_width;
renderRelease(start.x, start.y, last_drawn_width, bend_height);
}
}
renderText(start.x + last_drawn_width, bend.text);
last_bend = bend;
last_bend_draw_width = bend.draw_width;
last_bend.x = start.x;
start.x += last_drawn_width;
}
if (!last_bend || last_bend.x == undefined) {
throw new RuntimeError('NoLastBendForBend', 'Internal error.');
}
// Final arrowhead and text
if (last_bend.type === Bend.UP) {
renderArrowHead(last_bend.x + last_drawn_width, bend_height, +1);
} else if (last_bend.type === Bend.DOWN) {
renderArrowHead(last_bend.x + last_drawn_width, start.y, -1);
}
}
}