VexFlow - 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 { Vex } from './vex';
import { Flow } from './tables';
import { StaveModifier } from './stavemodifier';
import { Glyph } from './glyph';

export class KeySignature extends StaveModifier {
  static get CATEGORY() { return 'keysignatures'; }

Space between natural and following accidental depending on vertical position

  static get accidentalSpacing() {
    return {
      '#': {
        above: 6,
        below: 4,
      },
      'b': {
        above: 4,
        below: 7,
      },
      'n': {
        above: 3,
        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, cancelKeySpec, alterKeySpec) {
    super();
    this.attrs.type = 'KeySignature';

    this.setKeySig(keySpec, cancelKeySpec, alterKeySpec);
    this.setPosition(StaveModifier.Position.BEGIN);
    this.glyphFontScale = 38; // TODO(0xFE): Should this match StaveNote?
    this.glyphs = [];
    this.xPositions = []; // relative to this.x
    this.paddingForced = false;
  }

  getCategory() { return KeySignature.CATEGORY; }

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, nextAcc) {
    const accGlyphData = Flow.accidentalCodes(acc.type);
    const glyph = new Glyph(accGlyphData.code, this.glyphFontScale);

Determine spacing between current accidental and the next accidental

    let extraWidth = 0;
    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.stave, acc.line);
    this.glyphs.push(glyph);

    const xPosition = this.xPositions[this.xPositions.length - 1];
    const glyphWidth = accGlyphData.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) {
    this.formatted = false;
    this.cancelKeySpec = spec;

    return this;
  }

  convertToCancelAccList(spec) {

Get the accidental list for the cancelled key signature

    const cancel_accList = Flow.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;

Get the line position for each natural

    const cancelled = [];
    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);
  }

Deprecated

  addToStave(stave) {
    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, type) {
    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 < this.accList.length; ++i) {
        this.accList[i].line = customLines[i];
      }
    } else if (offset !== 0) {
      for (i = 0; i < this.accList.length; ++i) {
        this.accList[i].line += offset;
      }
    }
  }

  getPadding(index) {
    if (!this.formatted) this.format();

    return (
      this.glyphs.length === 0 || (!this.paddingForced && index < 2) ?
        0 : this.padding
    );
  }

  getWidth() {
    if (!this.formatted) this.format();

    return this.width;
  }

  setKeySig(keySpec, cancelKeySpec, alterKeySpec) {
    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) {
    this.formatted = false;
    this.alterKeySpec = alterKeySpec;

    return this;
  }

  convertToAlterAccList(alterKeySpec) {
    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() {
    if (!this.stave) {
      throw new Vex.RERR('KeySignatureError', "Can't draw key signature without stave.");
    }

    this.width = 0;
    this.glyphs = [];
    this.xPositions = [0]; // initialize with initial x position
    this.accList = Flow.keySignature(this.keySpec);
    if (this.cancelKeySpec) {
      this.convertToCancelAccList(this.cancelKeySpec);
    }
    const firstAccidentalType = this.accList.length > 0 ? this.accList[0].type : null;
    if (this.alterKeySpec) {
      this.convertToAlterAccList(this.alterKeySpec);
    }

    if (this.accList.length > 0) {
      this.convertAccLines(this.stave.clef, firstAccidentalType);
      for (let i = 0; i < this.accList.length; ++i) {
        this.convertToGlyph(this.accList[i], this.accList[i + 1]);
      }
    }

    this.formatted = true;
  }

  draw() {
    if (!this.x) {
      throw new Vex.RERR('KeySignatureError', "Can't draw key signature without x.");
    }

    if (!this.stave) {
      throw new Vex.RERR('KeySignatureError', "Can't draw key signature without stave.");
    }

    if (!this.formatted) this.format();

    for (let i = 0; i < this.glyphs.length; i++) {
      const glyph = this.glyphs[i];
      const x = this.x + this.xPositions[i];
      glyph.setStave(this.stave);
      glyph.setContext(this.stave.context);
      glyph.renderToStave(x);
    }
  }
}
h