UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature.

334 lines (291 loc) 9.43 kB
// [VexFlow](https://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // Author: Cyril Silverman // // ## Description // // This file implements key signatures. A key signature sits on a stave // and indicates the notes with implicit accidentals. import { Glyph } from './glyph'; import { Stave } from './stave'; import { StaveModifier, StaveModifierPosition } from './stavemodifier'; import { Tables } from './tables'; import { Category } from './typeguard'; import { defined } from './util'; export class KeySignature extends StaveModifier { static get CATEGORY(): string { return Category.KeySignature; } protected glyphFontScale: number; protected glyphs: Glyph[]; protected xPositions: number[]; protected paddingForced: boolean; protected formatted?: boolean; protected cancelKeySpec?: string; protected accList: { type: string; line: number }[] = []; protected keySpec?: string; protected alterKeySpec?: string[]; // Space between natural and following accidental depending // on vertical position static accidentalSpacing: Record<string, { above: number; below: number }> = { '#': { above: 6, below: 4, }, b: { above: 4, below: 7, }, n: { above: 4, below: 1, }, '##': { above: 6, below: 4, }, bb: { above: 4, below: 7, }, db: { above: 4, below: 7, }, d: { above: 4, below: 7, }, bbs: { above: 4, below: 7, }, '++': { above: 6, below: 4, }, '+': { above: 6, below: 4, }, '+-': { above: 6, below: 4, }, '++-': { above: 6, below: 4, }, bs: { above: 4, below: 10, }, bss: { above: 4, below: 10, }, }; // Create a new Key Signature based on a `key_spec` constructor(keySpec: string, cancelKeySpec?: string, alterKeySpec?: string[]) { super(); this.setKeySig(keySpec, cancelKeySpec, alterKeySpec); this.setPosition(StaveModifierPosition.BEGIN); this.glyphFontScale = Tables.NOTATION_FONT_SCALE; this.glyphs = []; this.xPositions = []; // relative to this.x this.paddingForced = false; } // Add an accidental glyph to the `KeySignature` instance which represents // the provided `acc`. If `nextAcc` is also provided, the appropriate // spacing will be included in the glyph's position convertToGlyph(acc: { type: string; line: number }, nextAcc: { type: string; line: number }): void { const accGlyphData = Tables.accidentalCodes(acc.type); const glyph = new Glyph(accGlyphData.code, this.glyphFontScale); // Determine spacing between current accidental and the next accidental let extraWidth = 1; if (acc.type === 'n' && nextAcc) { const spacing = KeySignature.accidentalSpacing[nextAcc.type]; if (spacing) { const isAbove = nextAcc.line >= acc.line; extraWidth = isAbove ? spacing.above : spacing.below; } } // Place the glyph on the stave this.placeGlyphOnLine(glyph, this.checkStave(), acc.line); this.glyphs.push(glyph); const xPosition = this.xPositions[this.xPositions.length - 1]; const glyphWidth = glyph.getMetrics().width + extraWidth; // Store the next accidental's x position this.xPositions.push(xPosition + glyphWidth); // Expand size of key signature this.width += glyphWidth; } // Cancel out a key signature provided in the `spec` parameter. This will // place appropriate natural accidentals before the key signature. cancelKey(spec: string): this { this.formatted = false; this.cancelKeySpec = spec; return this; } convertToCancelAccList(spec: string): { type: string; accList: { type: string; line: number }[] } | undefined { // Get the accidental list for the cancelled key signature const cancel_accList = Tables.keySignature(spec); // If the cancelled key has a different accidental type, ie: # vs b const different_types = this.accList.length > 0 && cancel_accList.length > 0 && cancel_accList[0].type !== this.accList[0].type; // Determine how many naturals needed to add const naturals = different_types ? cancel_accList.length : cancel_accList.length - this.accList.length; // Return if no naturals needed if (naturals < 1) return undefined; // Get the line position for each natural const cancelled: { type: string; line: number }[] = []; for (let i = 0; i < naturals; i++) { let index = i; if (!different_types) { index = cancel_accList.length - naturals + i; } const acc = cancel_accList[index]; cancelled.push({ type: 'n', line: acc.line }); } // Combine naturals with main accidental list for the key signature this.accList = cancelled.concat(this.accList); return { accList: cancelled, type: cancel_accList[0].type, }; } // Deprecated addToStave(stave: Stave): this { this.paddingForced = true; stave.addModifier(this); return this; } // Apply the accidental staff line placement based on the `clef` and // the accidental `type` for the key signature ('# or 'b'). convertAccLines(clef: string, type?: string, accList = this.accList): void { let offset = 0.0; // if clef === "treble" let customLines; // when clef doesn't follow treble key sig shape switch (clef) { // Treble & Subbass both have offsets of 0, so are not included. case 'soprano': if (type === '#') customLines = [2.5, 0.5, 2, 0, 1.5, -0.5, 1]; else offset = -1; break; case 'mezzo-soprano': if (type === 'b') customLines = [0, 2, 0.5, 2.5, 1, 3, 1.5]; else offset = 1.5; break; case 'alto': offset = 0.5; break; case 'tenor': if (type === '#') customLines = [3, 1, 2.5, 0.5, 2, 0, 1.5]; else offset = -0.5; break; case 'baritone-f': case 'baritone-c': if (type === 'b') customLines = [0.5, 2.5, 1, 3, 1.5, 3.5, 2]; else offset = 2; break; case 'bass': case 'french': offset = 1; break; default: break; } // If there's a special case, assign those lines/spaces: let i; if (typeof customLines !== 'undefined') { for (i = 0; i < accList.length; ++i) { accList[i].line = customLines[i]; } } else if (offset !== 0) { for (i = 0; i < accList.length; ++i) { accList[i].line += offset; } } } getPadding(index: number): number { if (!this.formatted) this.format(); return this.glyphs.length === 0 || (!this.paddingForced && index < 2) ? 0 : this.padding; } getWidth(): number { if (!this.formatted) this.format(); return this.width; } setKeySig(keySpec: string, cancelKeySpec?: string, alterKeySpec?: string[]): this { this.formatted = false; this.keySpec = keySpec; this.cancelKeySpec = cancelKeySpec; this.alterKeySpec = alterKeySpec; return this; } // Alter the accidentals of a key spec one by one. // Each alteration is a new accidental that replaces the // original accidental (or the canceled one). alterKey(alterKeySpec: string[]): this { this.formatted = false; this.alterKeySpec = alterKeySpec; return this; } convertToAlterAccList(alterKeySpec: string[]): void { const max = Math.min(alterKeySpec.length, this.accList.length); for (let i = 0; i < max; ++i) { if (alterKeySpec[i]) { this.accList[i].type = alterKeySpec[i]; } } } format(): void { const stave = this.checkStave(); this.width = 0; this.glyphs = []; this.xPositions = [0]; // initialize with initial x position this.accList = Tables.keySignature(defined(this.keySpec)); const accList = this.accList; const firstAccidentalType = accList.length > 0 ? accList[0].type : undefined; let cancelAccList; if (this.cancelKeySpec) { cancelAccList = this.convertToCancelAccList(this.cancelKeySpec); } if (this.alterKeySpec) { this.convertToAlterAccList(this.alterKeySpec); } if (this.accList.length > 0) { const clef = (this.position === StaveModifierPosition.END ? stave.getEndClef() : stave.getClef()) || stave.getClef(); if (cancelAccList) { this.convertAccLines(clef, cancelAccList.type, cancelAccList.accList); } this.convertAccLines(clef, firstAccidentalType, accList); for (let i = 0; i < this.accList.length; ++i) { this.convertToGlyph(this.accList[i], this.accList[i + 1]); } } this.formatted = true; } /** * Return the Glyph objects making up this KeySignature. */ getGlyphs(): Glyph[] { if (!this.formatted) this.format(); return this.glyphs; } draw(): void { const stave = this.checkStave(); const ctx = stave.checkContext(); if (!this.formatted) this.format(); this.setRendered(); this.applyStyle(ctx); ctx.openGroup('keysignature', this.getAttribute('id')); for (let i = 0; i < this.glyphs.length; i++) { const glyph = this.glyphs[i]; const x = this.x + this.xPositions[i]; glyph.setStave(stave); glyph.setContext(ctx); glyph.renderToStave(x); } ctx.closeGroup(); this.restoreStyle(ctx); } }