vexflow
Version:
A JavaScript library for rendering music notation and guitar tablature.
223 lines (193 loc) • 6.16 kB
text/typescript
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
//
// ## Description
// Author: Raffaele Viglianti, 2012 http://itisnotsound.wordpress.com/
//
// This class implements hairpins between notes.
// Hairpins can be either crescendo or decrescendo.
import { Element } from './element';
import { Modifier } from './modifier';
import { Note } from './note';
import { RenderContext } from './rendercontext';
import { Category } from './typeguard';
import { RuntimeError } from './util';
export interface StaveHairpinRenderOptions {
right_shift_ticks?: number;
left_shift_ticks?: number;
left_shift_px: number;
right_shift_px: number;
height: number;
y_shift: number;
}
export class StaveHairpin extends Element {
static get CATEGORY(): string {
return Category.StaveHairpin;
}
protected hairpin: number;
protected position: number;
public render_options: StaveHairpinRenderOptions;
// notes is initialized by the constructor via this.setNotes(notes).
protected notes!: Record<string, Note>;
protected first_note?: Note;
protected last_note?: Note;
static readonly type = {
CRESC: 1,
DECRESC: 2,
};
/* Helper function to convert ticks into pixels.
* Requires a Formatter with voices joined and formatted (to
* get pixels per tick)
*
* options is struct that has:
*
* {
* height: px,
* y_shift: px, // vertical offset
* left_shift_ticks: 0, // left horizontal offset expressed in ticks
* right_shift_ticks: 0 // right horizontal offset expressed in ticks
* }
*
**/
static FormatByTicksAndDraw(
ctx: RenderContext,
formatter: { pixelsPerTick: number },
notes: Record<string, Note>,
type: number,
position: number,
options: StaveHairpinRenderOptions
): void {
const ppt = formatter.pixelsPerTick;
if (ppt == null) {
throw new RuntimeError('BadArguments', 'A valid Formatter must be provide to draw offsets by ticks.');
}
const l_shift_px = ppt * (options.left_shift_ticks ?? 0);
const r_shift_px = ppt * (options.right_shift_ticks ?? 0);
const hairpin_options = {
height: options.height,
y_shift: options.y_shift,
left_shift_px: l_shift_px,
right_shift_px: r_shift_px,
right_shift_ticks: 0,
left_shift_ticks: 0,
};
new StaveHairpin(
{
first_note: notes.first_note,
last_note: notes.last_note,
},
type
)
.setContext(ctx)
.setRenderOptions(hairpin_options)
.setPosition(position)
.draw();
}
/**
* Create a new hairpin from the specified notes.
*
* @param {!Object} notes The notes to tie up.
* Notes is a struct that has:
*
* {
* first_note: Note,
* last_note: Note,
* }
* @param {!Object} type The type of hairpin
*/
constructor(notes: Record<string, Note>, type: number) {
super();
this.setNotes(notes);
this.hairpin = type;
this.position = Modifier.Position.BELOW;
this.render_options = {
height: 10,
y_shift: 0, // vertical offset
left_shift_px: 0, // left horizontal offset
right_shift_px: 0, // right horizontal offset
right_shift_ticks: 0,
left_shift_ticks: 0,
};
}
setPosition(position: number): this {
if (position === Modifier.Position.ABOVE || position === Modifier.Position.BELOW) {
this.position = position;
}
return this;
}
setRenderOptions(options: StaveHairpinRenderOptions): this {
if (
options.height != null &&
options.y_shift != null &&
options.left_shift_px != null &&
options.right_shift_px != null
) {
this.render_options = options;
}
return this;
}
/**
* Set the notes to attach this hairpin to.
*
* @param {!Object} notes The start and end notes.
*/
setNotes(notes: Record<string, Note>): this {
if (!notes.first_note && !notes.last_note) {
throw new RuntimeError('BadArguments', 'Hairpin needs to have either first_note or last_note set.');
}
this.notes = notes;
this.first_note = notes.first_note;
this.last_note = notes.last_note;
return this;
}
renderHairpin(params: {
first_x: number;
last_x: number;
first_y: number;
last_y: number;
staff_height: number;
}): void {
const ctx = this.checkContext();
let dis = this.render_options.y_shift + 20;
let y_shift = params.first_y;
if (this.position === Modifier.Position.ABOVE) {
dis = -dis + 30;
y_shift = params.first_y - params.staff_height;
}
const l_shift = this.render_options.left_shift_px;
const r_shift = this.render_options.right_shift_px;
ctx.beginPath();
switch (this.hairpin) {
case StaveHairpin.type.CRESC:
ctx.moveTo(params.last_x + r_shift, y_shift + dis);
ctx.lineTo(params.first_x + l_shift, y_shift + this.render_options.height / 2 + dis);
ctx.lineTo(params.last_x + r_shift, y_shift + this.render_options.height + dis);
break;
case StaveHairpin.type.DECRESC:
ctx.moveTo(params.first_x + l_shift, y_shift + dis);
ctx.lineTo(params.last_x + r_shift, y_shift + this.render_options.height / 2 + dis);
ctx.lineTo(params.first_x + l_shift, y_shift + this.render_options.height + dis);
break;
default:
// Default is NONE, so nothing to draw
break;
}
ctx.stroke();
ctx.closePath();
}
draw(): void {
this.checkContext();
this.setRendered();
const firstNote = this.first_note;
const lastNote = this.last_note;
if (!firstNote || !lastNote) throw new RuntimeError('NoNote', 'Notes required to draw');
const start = firstNote.getModifierStartXY(this.position, 0);
const end = lastNote.getModifierStartXY(this.position, 0);
this.renderHairpin({
first_x: start.x,
last_x: end.x,
first_y: firstNote.checkStave().getY() + firstNote.checkStave().getHeight(),
last_y: lastNote.checkStave().getY() + lastNote.checkStave().getHeight(),
staff_height: firstNote.checkStave().getHeight(),
});
}
}