UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

705 lines (622 loc) 21.1 kB
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // @author Mohit Cheppudira // MIT License import { Accidental } from './accidental'; import { Annotation, AnnotationHorizontalJustify, AnnotationVerticalJustify } from './annotation'; import { Articulation } from './articulation'; import { BarNote } from './barnote'; import { Beam, PartialBeamDirection } from './beam'; import { ChordSymbol } from './chordsymbol'; import { ClefNote } from './clefnote'; import { Curve, CurveOptions } from './curve'; import { EasyScore, EasyScoreOptions } from './easyscore'; import { Element } from './element'; import { FontInfo } from './font'; import { Formatter, FormatterOptions } from './formatter'; import { FretHandFinger } from './frethandfinger'; import { GhostNote } from './ghostnote'; import { Glyph } from './glyph'; import { GlyphNote, GlyphNoteOptions } from './glyphnote'; import { GraceNote, GraceNoteStruct } from './gracenote'; import { GraceNoteGroup } from './gracenotegroup'; import { KeySigNote } from './keysignote'; import { ModifierContext } from './modifiercontext'; import { MultiMeasureRest, MultimeasureRestRenderOptions } from './multimeasurerest'; import { Note, NoteStruct } from './note'; import { NoteSubGroup } from './notesubgroup'; import { Ornament } from './ornament'; import { PedalMarking } from './pedalmarking'; import { RenderContext } from './rendercontext'; import { Renderer } from './renderer'; import { RepeatNote } from './repeatnote'; import { Stave, StaveOptions } from './stave'; import { BarlineType } from './stavebarline'; import { StaveConnector, StaveConnectorType } from './staveconnector'; import { StaveLine } from './staveline'; import { StaveNote, StaveNoteStruct } from './stavenote'; import { StaveTie } from './stavetie'; import { StemmableNote } from './stemmablenote'; import { StringNumber } from './stringnumber'; import { System, SystemOptions } from './system'; import { TabNote, TabNoteStruct } from './tabnote'; import { TabStave } from './tabstave'; import { TextBracket } from './textbracket'; import { TextDynamics } from './textdynamics'; import { TextNote, TextNoteStruct } from './textnote'; import { TickContext } from './tickcontext'; import { TimeSigNote } from './timesignote'; import { Tuplet, TupletOptions } from './tuplet'; import { defined, log, RuntimeError } from './util'; import { VibratoBracket } from './vibratobracket'; import { Voice, VoiceTime } from './voice'; import { isHTMLCanvas } from './web'; export interface FactoryOptions { stave?: { space: number; }; renderer?: { elementId: string | null; backend?: number; width: number; height: number; background?: string; }; font?: FontInfo; } // eslint-disable-next-line function L(...args: any[]) { if (Factory.DEBUG) log('Vex.Flow.Factory', args); } /** * Factory implements a high level API around VexFlow. */ export class Factory { /** To enable logging for this class. Set `Vex.Flow.Factory.DEBUG` to `true`. */ static DEBUG: boolean = false; /** Default text font. */ static TEXT_FONT: Required<FontInfo> = { ...Element.TEXT_FONT }; /** * Static simplified function to access constructor without providing FactoryOptions * * Example: * * Create an SVG renderer and attach it to the DIV element named "boo" to render using <page-width> 1200 and <page-height> 600 * * `const vf: Factory = Vex.Flow.Factory.newFromElementId('boo', 1200, 600 );` */ static newFromElementId(elementId: string | null, width = 500, height = 200): Factory { return new Factory({ renderer: { elementId, width, height } }); } protected options: Required<FactoryOptions>; protected stave?: Stave; protected context!: RenderContext; protected staves!: Stave[]; protected voices!: Voice[]; protected renderQ!: Element[]; protected systems!: System[]; /** * Example: * * Create an SVG renderer and attach it to the DIV element named "boo" to render using <page-width> 1200 and <page-height> 600 * * `const vf: Factory = new Vex.Flow.Factory({renderer: { elementId: 'boo', width: 1200, height: 600 }});` */ constructor(options: FactoryOptions = {}) { L('New factory: ', options); this.options = { stave: { space: 10, }, renderer: { elementId: '', width: 500, height: 200, background: '#FFF', }, font: Factory.TEXT_FONT, }; this.setOptions(options); } reset(): void { this.renderQ = []; this.systems = []; this.staves = []; this.voices = []; this.stave = undefined; // current stave } setOptions(options?: FactoryOptions): void { this.options = { ...this.options, ...options }; this.initRenderer(); this.reset(); } initRenderer(): void { const { elementId, width, height, background } = this.options.renderer; if (elementId == null) { return; } if (elementId == '') { L(this); throw new RuntimeError('renderer.elementId not set in FactoryOptions'); } let backend = this.options.renderer.backend; if (backend === undefined) { const elem = document.getElementById(elementId); // We use a custom type check here, because node-canvas mimics canvas, // but is not an instance of window.HTMLCanvasElement. // In fact, `window` might be undefined here. // See: https://www.npmjs.com/package/canvas if (isHTMLCanvas(elem)) { backend = Renderer.Backends.CANVAS; } else { backend = Renderer.Backends.SVG; } } this.context = Renderer.buildContext(elementId, backend, width, height, background); } getContext(): RenderContext { return this.context; } setContext(context: RenderContext): this { this.context = context; return this; } getStave(): Stave | undefined { return this.stave; } getVoices(): Voice[] { return this.voices; } /** Return pixels from current stave spacing. */ Stave(params?: { x?: number; y?: number; width?: number; options?: StaveOptions }): Stave { const staveSpace = this.options.stave.space; const p = { x: 0, y: 0, width: this.options.renderer.width - staveSpace * 1.0, options: { spacing_between_lines_px: staveSpace * 1.0 }, ...params, }; const stave: Stave = new Stave(p.x, p.y, p.width, p.options); this.staves.push(stave); stave.setContext(this.context); this.stave = stave; return stave; } TabStave(params?: { x?: number; y?: number; width?: number; options?: StaveOptions }): TabStave { const staveSpace = this.options.stave.space; const p = { x: 0, y: 0, width: this.options.renderer.width - staveSpace * 1.0, options: { spacing_between_lines_px: staveSpace * 1.3 }, ...params, }; const stave = new TabStave(p.x, p.y, p.width, p.options); this.staves.push(stave); stave.setContext(this.context); this.stave = stave; return stave; } StaveNote(noteStruct: StaveNoteStruct): StaveNote { const note = new StaveNote(noteStruct); if (this.stave) note.setStave(this.stave); note.setContext(this.context); this.renderQ.push(note); return note; } GlyphNote(glyph: Glyph, noteStruct: NoteStruct, options?: GlyphNoteOptions): GlyphNote { const note = new GlyphNote(glyph, noteStruct, options); if (this.stave) note.setStave(this.stave); note.setContext(this.context); this.renderQ.push(note); return note; } RepeatNote(type: string, noteStruct?: NoteStruct, options?: GlyphNoteOptions): RepeatNote { const note = new RepeatNote(type, noteStruct, options); if (this.stave) note.setStave(this.stave); note.setContext(this.context); this.renderQ.push(note); return note; } GhostNote(noteStruct: string | NoteStruct): GhostNote { const ghostNote = new GhostNote(noteStruct); if (this.stave) ghostNote.setStave(this.stave); ghostNote.setContext(this.context); this.renderQ.push(ghostNote); return ghostNote; } TextNote(noteStruct: TextNoteStruct): TextNote { const textNote = new TextNote(noteStruct); if (this.stave) textNote.setStave(this.stave); textNote.setContext(this.context); this.renderQ.push(textNote); return textNote; } BarNote(params: { type?: BarlineType | string } = {}): BarNote { const barNote = new BarNote(params.type); if (this.stave) barNote.setStave(this.stave); barNote.setContext(this.context); this.renderQ.push(barNote); return barNote; } ClefNote(params?: { type?: string; options?: { size?: string; annotation?: string } }): ClefNote { const p = { type: 'treble', options: { size: 'default', annotation: undefined, }, ...params, }; const clefNote = new ClefNote(p.type, p.options.size, p.options.annotation); if (this.stave) clefNote.setStave(this.stave); clefNote.setContext(this.context); this.renderQ.push(clefNote); return clefNote; } TimeSigNote(params?: { time?: string }): TimeSigNote { const p = { time: '4/4', ...params, }; const timeSigNote = new TimeSigNote(p.time); if (this.stave) timeSigNote.setStave(this.stave); timeSigNote.setContext(this.context); this.renderQ.push(timeSigNote); return timeSigNote; } KeySigNote(params: { key: string; cancelKey?: string; alterKey?: string[] }): KeySigNote { const keySigNote = new KeySigNote(params.key, params.cancelKey, params.alterKey); if (this.stave) keySigNote.setStave(this.stave); keySigNote.setContext(this.context); this.renderQ.push(keySigNote); return keySigNote; } TabNote(noteStruct: TabNoteStruct): TabNote { const note = new TabNote(noteStruct); if (this.stave) note.setStave(this.stave); note.setContext(this.context); this.renderQ.push(note); return note; } GraceNote(noteStruct: GraceNoteStruct): GraceNote { const note = new GraceNote(noteStruct); if (this.stave) note.setStave(this.stave); note.setContext(this.context); return note; } GraceNoteGroup(params: { notes: StemmableNote[]; slur?: boolean }): GraceNoteGroup { const group = new GraceNoteGroup(params.notes, params.slur); group.setContext(this.context); return group; } Accidental(params: { type: string }): Accidental { const accid = new Accidental(params.type); accid.setContext(this.context); return accid; } Annotation(params?: { text?: string; hJustify?: string | AnnotationHorizontalJustify; vJustify?: string | AnnotationVerticalJustify; font?: FontInfo; }): Annotation { const p = { text: 'p', hJustify: AnnotationHorizontalJustify.CENTER, vJustify: AnnotationVerticalJustify.BOTTOM, ...params, }; const annotation = new Annotation(p.text); annotation.setJustification(p.hJustify); annotation.setVerticalJustification(p.vJustify); annotation.setFont(p.font); annotation.setContext(this.context); return annotation; } ChordSymbol(params?: { vJustify?: string; hJustify?: string; kerning?: boolean; reportWidth?: boolean; fontFamily?: string; fontSize?: number; fontWeight?: string; }): ChordSymbol { const p = { vJustify: 'top', hJustify: 'center', kerning: true, reportWidth: true, ...params, }; const chordSymbol = new ChordSymbol(); chordSymbol.setHorizontal(p.hJustify); chordSymbol.setVertical(p.vJustify); chordSymbol.setEnableKerning(p.kerning); chordSymbol.setReportWidth(p.reportWidth); // There is a default font based on the engraving font. Only set then // font if it is specific, else use the default if (typeof p.fontFamily === 'string' && typeof p.fontSize === 'number') { if (typeof p.fontWeight === 'string') chordSymbol.setFont(p.fontFamily, p.fontSize, p.fontWeight); else chordSymbol.setFont(p.fontFamily, p.fontSize, ''); } else if (typeof p.fontSize === 'number') { chordSymbol.setFontSize(p.fontSize); } chordSymbol.setContext(this.context); return chordSymbol; } Articulation(params?: { betweenLines?: boolean; type?: string; position?: string | number }): Articulation { const articulation = new Articulation(params?.type ?? 'a.'); if (params?.position != undefined) articulation.setPosition(params.position); if (params?.betweenLines != undefined) articulation.setBetweenLines(params.betweenLines); articulation.setContext(this.context); return articulation; } Ornament( type: string, params?: { position?: string | number; upperAccidental?: string; lowerAccidental?: string; delayed?: boolean } ) { const options = { type, position: 0, accidental: '', ...params, }; const ornament = new Ornament(type); ornament.setPosition(options.position); if (options.upperAccidental) { ornament.setUpperAccidental(options.upperAccidental); } if (options.lowerAccidental) { ornament.setLowerAccidental(options.lowerAccidental); } if (typeof options.delayed !== 'undefined') { ornament.setDelayed(options.delayed); } ornament.setContext(this.context); return ornament; } TextDynamics(params?: { text?: string; duration?: string; dots?: number; line?: number }): TextDynamics { const p = { text: 'p', duration: 'q', dots: 0, line: 0, ...params, }; const text = new TextDynamics({ text: p.text, line: p.line, duration: p.duration, dots: p.dots, }); if (this.stave) text.setStave(this.stave); text.setContext(this.context); this.renderQ.push(text); return text; } Fingering(params: { number?: string; position?: string }): FretHandFinger { const p = { number: '0', position: 'left', ...params, }; const fingering = new FretHandFinger(p.number); fingering.setPosition(p.position); fingering.setContext(this.context); return fingering; } StringNumber(params: { number: string; position: string }, drawCircle = true): StringNumber { const stringNumber = new StringNumber(params.number); stringNumber.setPosition(params.position); stringNumber.setContext(this.context); stringNumber.setDrawCircle(drawCircle); return stringNumber; } TickContext(): TickContext { return new TickContext(); } ModifierContext(): ModifierContext { return new ModifierContext(); } MultiMeasureRest(params: MultimeasureRestRenderOptions): MultiMeasureRest { const numMeasures = defined(params.number_of_measures, 'NoNumberOfMeasures'); const multiMeasureRest = new MultiMeasureRest(numMeasures, params); multiMeasureRest.setContext(this.context); this.renderQ.push(multiMeasureRest); return multiMeasureRest; } Voice(params?: { time?: VoiceTime | string }): Voice { const p = { time: '4/4', ...params, }; const voice = new Voice(p.time); this.voices.push(voice); return voice; } StaveConnector(params: { top_stave: Stave; bottom_stave: Stave; type: StaveConnectorType }): StaveConnector { const connector = new StaveConnector(params.top_stave, params.bottom_stave); connector.setType(params.type).setContext(this.context); this.renderQ.push(connector); return connector; } Formatter(options?: FormatterOptions): Formatter { return new Formatter(options); } Tuplet(params?: { notes?: Note[]; options?: TupletOptions }): Tuplet { const p = { notes: [], options: {}, ...params, }; const tuplet = new Tuplet(p.notes, p.options).setContext(this.context); this.renderQ.push(tuplet); return tuplet; } Beam(params: { notes: StemmableNote[]; options?: { autoStem?: boolean; secondaryBeamBreaks?: number[]; partialBeamDirections?: { [noteIndex: number]: PartialBeamDirection; }; }; }): Beam { const beam = new Beam(params.notes, params.options?.autoStem).setContext(this.context); beam.breakSecondaryAt(params.options?.secondaryBeamBreaks ?? []); if (params.options?.partialBeamDirections) { Object.entries(params.options?.partialBeamDirections).forEach(([noteIndex, direction]) => { beam.setPartialBeamSideAt(Number(noteIndex), direction); }); } this.renderQ.push(beam); return beam; } Curve(params: { from: Note; to: Note; options: CurveOptions }): Curve { const curve = new Curve(params.from, params.to, params.options).setContext(this.context); this.renderQ.push(curve); return curve; } StaveTie(params: { from?: Note | null; to?: Note | null; first_indices?: number[]; last_indices?: number[]; text?: string; options?: { direction?: number }; }): StaveTie { const tie = new StaveTie( { first_note: params.from, last_note: params.to, first_indices: params.first_indices, last_indices: params.last_indices, }, params.text ); if (params.options?.direction) tie.setDirection(params.options.direction); tie.setContext(this.context); this.renderQ.push(tie); return tie; } StaveLine(params: { from: StaveNote; to: StaveNote; first_indices: number[]; last_indices: number[]; options?: { text?: string; font?: FontInfo }; }): StaveLine { const line = new StaveLine({ first_note: params.from, last_note: params.to, first_indices: params.first_indices, last_indices: params.last_indices, }); if (params.options?.text) line.setText(params.options.text); if (params.options?.font) line.setFont(params.options.font); line.setContext(this.context); this.renderQ.push(line); return line; } VibratoBracket(params: { from: Note | null; to: Note | null; options: { harsh?: boolean; line?: number; }; }): VibratoBracket { const vibratoBracket = new VibratoBracket({ start: params.from, stop: params.to, }); if (params.options.line) vibratoBracket.setLine(params.options.line); if (params.options.harsh) vibratoBracket.setHarsh(params.options.harsh); vibratoBracket.setContext(this.context); this.renderQ.push(vibratoBracket); return vibratoBracket; } TextBracket(params: { from: Note; to: Note; text: string; options: { superscript: string; position: string; line?: number; font?: FontInfo; }; }): TextBracket { const textBracket = new TextBracket({ start: params.from, stop: params.to, text: params.text, superscript: params.options.superscript, position: params.options.position, }); if (params.options.line) textBracket.setLine(params.options.line); if (params.options.font) textBracket.setFont(params.options.font); textBracket.setContext(this.context); this.renderQ.push(textBracket); return textBracket; } System(params: SystemOptions = {}): System { params.factory = this; const system = new System(params).setContext(this.context); this.systems.push(system); return system; } /** * Creates EasyScore. Normally the first step after constructing a Factory. For example: * ``` * const vf: Factory = new Vex.Flow.Factory({renderer: { elementId: 'boo', width: 1200, height: 600 }}); * const score: EasyScore = vf.EasyScore(); * ``` * @param options.factory optional instance of Factory * @param options.builder instance of Builder * @param options.commitHooks function to call after a note element is created * @param options.throwOnError throw error in case of parsing error */ EasyScore(options: EasyScoreOptions = {}): EasyScore { options.factory = this; return new EasyScore(options); } PedalMarking(params?: { notes?: StaveNote[]; options?: { style: string } }): PedalMarking { const p = { notes: [], options: { style: 'mixed', }, ...params, }; const pedal = new PedalMarking(p.notes); pedal.setType(PedalMarking.typeString[p.options.style]); pedal.setContext(this.context); this.renderQ.push(pedal); return pedal; } NoteSubGroup(params?: { notes?: Note[] }): NoteSubGroup { const p = { notes: [], ...params, }; const group = new NoteSubGroup(p.notes); group.setContext(this.context); return group; } /** Render the score. */ draw(): void { const ctx = this.context; this.systems.forEach((s) => s.setContext(ctx).format()); this.staves.forEach((s) => s.setContext(ctx).draw()); this.voices.forEach((v) => v.setContext(ctx).draw()); this.renderQ.forEach((e) => { if (!e.isRendered()) e.setContext(ctx).draw(); }); this.systems.forEach((s) => s.setContext(ctx).draw()); this.reset(); } }