Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for tuplets and beams #4

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@
</vf-stave>
</vf-system>
</vf-score>

<vf-score height=300>
<vf-system>
<vf-stave clef='treble'>
<vf-voice stem='up'>
<vf-tuplet>C5/q, B4, A4</vf-tuplet>
<vf-tuplet ratioed=true beamed>d4/8, g4, b4, d4, a4</vf-tuplet>
E4/q
</vf-voice>
</vf-stave>
<vf-stave clef='bass'>
<vf-voice stem='down'>
<vf-beam stem='down'>c3/8, f3, d3, g3</vf-beam>
<vf-beam>d4/16, f3, d3, g3</vf-beam>
g3/q
</vf-voice>
</vf-stave>
</vf-system>
</vf-score>

</body>
</html>
2 changes: 2 additions & 0 deletions src/events/elementReadyEvent.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
// dispatch to their parent element when they have finished creating its elements.

export default class ElementReadyEvent extends Event {
static beamReadyEventName = 'vf-beam-ready';
static staveReadyEventName = 'vf-stave-ready';
static systemReadyEventName = 'vf-system-ready';
static tupletReadyEventName = 'vf-tuplet-ready';
static voiceReadyEventName = 'vf-voice-ready';

constructor(eventName) {
Expand Down
11 changes: 11 additions & 0 deletions src/events/getParentStemEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* This file implements `GetParentStemEvent`, the event that vf-tuplet elements
* dispatch in order to get the stem direction of their parent voice.
*/
export default class GetParentStemEvent extends Event {
static eventName = 'get-parent-stem';

constructor() {
super(GetParentStemEvent.eventName, { bubbles: true });
}
}
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { VFScore } from './vf-score';
export { VFSystem } from './vf-system';
export { VFStave } from './vf-stave';
export { VFVoice } from './vf-voice';
export { VFVoice } from './vf-voice';
export { VFTuplet } from './vf-tuplet';
export { VFBeam } from './vf-beam';
33 changes: 33 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Creates StavesNotes from the provided text, utlizing the provided EasyScore
* instance. If the notes have their own stem direction provided in the text
* (e.g. C4/q[stem='down]), the note's stem direction takes precendence over
* the provided stemDirection.
*
* Utlizes the EasyScore API Grammar & Parser.
*
* @param {Vex.Flow.EasyScore} score - The EasyScore instance to use.
* @param {String} text - The string to parse and create notes from.
* @param {String} stemDirection - The stem direction for the notes. Individual
* notes can override this.
* @returns {[Vex.Flow.StaveNote]} - The notes that were generated from
* @param text .
*/
export function createNotesFromText(score, text, stemDirection) {
score.set({ stem: stemDirection });
const staveNotes = score.notes(text);
return staveNotes
}

/**
* Creates a Beam for the provides notes, utilizing the provided EasyScore
* instance.
*
* @param {Vex.Flow.EasyScore} score - The EasyScore instance to use.
* @param {[Vex.Flow.StaveNote]} notes - The StaveNotes to create a beam for.
* @returns {Vex.Flow.Beam} - The Beam generated to connect @param notes .
*/
export function createBeamForNotes(score, notes) {
const beam = score.beam(notes);
return beam;
}
90 changes: 90 additions & 0 deletions src/vf-beam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import './vf-voice';

import { createBeamForNotes, createNotesFromText } from './utils';
import ElementAddedEvent from './events/elementAddedEvent';
import ElementReadyEvent from './events/elementReadyEvent';
import GetParentStemEvent from './events/getParentStemEvent';

/**
* Implements `vf-beam`, the web component that closely resembles the `Beam`
* element.
* `vf-beam` is responsible for creating notes from its text content and the beam
* for those notes. One beam goes across all of the notes.
* Once the beam and notes are created, `vf-beam` dispatches an event to its parent
* `vf-voice` to signal that it's ready for its notes and beam to be added to
* the voice.
*/
export class VFBeam extends HTMLElement {

/**
* The Vex.Flow.EasyScore instance to use.
* @type {Vex.Flow.Registry}
* @private
*/
_score;

/**
* The direction of the stems in the beam.
* @type {String}
* @private
*/
stemDirection = 'up';

/**
* The notes that make up this vf-beam.
* @type {[Vex.Flow.StaveNote]}
*/
notes;

/**
* The beam for this vf-beam.
* @type {Vex.Flow.Beam}
*/
beam;

constructor() {
super();
}

connectedCallback() {
// If this vf-beam component provides its own stem direction, respect it.
// If it doesn't provide its own stem direction, use the stem direction of
// its parent vf-voice.
if (this.getAttribute('stem')) {
this.stemDirection = this.getAttribute('stem');
} else {
this.dispatchEvent(new GetParentStemEvent());
}

this.dispatchEvent(new ElementAddedEvent());
}

/**
* Setter to detect when the EasyScore instance is set. Once the EasyScore
* instances is set, vf-beam can start creating components.
*
* @param {Vex.Flow.EasyScore} value - The EasyScore instance that the parent
* stave and its children are using, set
* by the parent vf-stave.
*/
set score(value) {
this._score = value;
this.createNotesAndBeam();
}

/**
* Creates the StaveNotes and Beam from the text content of this vf-beam.
*/
createNotesAndBeam() {
this.notes = createNotesFromText(this._score, this.textContent, this.stemDirection);
this.beam = createBeamForNotes(this._score, this.notes);

/**
* Tell the parent vf-voice that this vf-beam has finished creating its
* notes and beam and is ready to be added to the voice.
*/
this.dispatchEvent(new ElementReadyEvent(ElementReadyEvent.beamReadyEventName));
}
}

window.customElements.define('vf-beam', VFBeam);
1 change: 0 additions & 1 deletion src/vf-score.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ export class VFScore extends HTMLElement {
this._y = parseInt(this.getAttribute('y')) || this._y;
this._rendererType = this.getAttribute('renderer') || this._rendererType;


// Because connectedCallback could be called multiple times, safeguard
// against setting up the renderer, factory, etc. more than once.
if (!this._isSetup) {
Expand Down
160 changes: 160 additions & 0 deletions src/vf-tuplet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import Vex from 'vexflow';

import './vf-voice';

import { createBeamForNotes, createNotesFromText } from './utils';
import ElementAddedEvent from './events/elementAddedEvent';
import ElementReadyEvent from './events/elementReadyEvent';
import GetParentStemEvent from './events/getParentStemEvent';

/**
* Implements `vf-tuplet`, the web component that closely resembles the `Tuplet`
* element.
* `vf-tuplet` is responsible for creating the tuplet, and optionally the beam,
* from its text content.
* Once the tuplet is created, `vf-tuplet` dispatches an event to its parent
* `vf-voice` to signal that it's ready for its tuplet (and beam) to be added to
* the voice.
*/
export class VFTuplet extends HTMLElement {

/**
* The Vex.Flow.EasyScore instance to use.
* @type {Vex.Flow.Registry}
* @private
*/
_score;

/**
* Boolean represented whether the notes in the tuplet should be attached
* by a beam.
* @type {Boolean}
* @private
*/
_beamed = false;

/**
* The direction of the stems in the tuplet. If the notes have their own stem
* direction in the text (e.g. C4/q[stem='down]), the note's stem direction
* takes precendence over this property.
* @type {String}
* @private
*/
stemDirection = 'up';

/**
* The location of the tuplet notation. Can be above or below the notes.
* @type {String}
* @private
*/
_location = 'above';

/**
* The notes that make up the tuplet.
* @type {[Vex.Flow.StaveNote]}
* @private
*/
_notes;

/**
* The tuplet created from the component's text content.
* @type {Vex.Flow.Tuplet}
*/
tuplet;

/**
* The beam for the tuplet. Undefined if the VFTuplet if _beamed = false;
* @type {Vex.Flow.Beam}
*/
beam;

constructor() {
super();
}

connectedCallback() {
this._beamed = this.hasAttribute('beamed');
this._location = this.getAttribute('location') || this._location;

// If this vf-tuplet component provides its own stem direction, respect it.
// If it doesn't provide its own stem direction, use the stem direction of
// its parent vf-voice.
if (this.getAttribute('stem')) {
this.stemDirection = this.getAttribute('stem');
} else {
this.dispatchEvent(new GetParentStemEvent());
}

this.dispatchEvent(new ElementAddedEvent());
}

static get observedAttributes() { return ['beamed', 'location', 'numNotes', 'notesOccupied'] }

attributeChangedCallback(name, oldValue, newValue) {
// TODO (ywsang): Implement code to update based on changes to attributes
}

/**
* Setter to detect when the EasyScore instance is set. Once the EasyScore
* instance is set, vf-tuplet can start creating components.
*
* @param {Vex.Flow.EasyScore} value - The EasyScore instance that the parent
* stave and its children are using, set
* by the parent vf-stave.
*/
set score(value) {
this._score = value;
this.createTuplet();
}

/**
* Creates a Vex.Flow.Tuplet from the textContent of the component.
*/
createTuplet() {
// this._createNotes(this.textContent);

this._notes = createNotesFromText(this._score, this.textContent, this.stemDirection);
// As in the original VexFlow library: If numNotes is not specified, default
// to num_notes = length of the notes the tuplet is created from.
const numNotes = (this.hasAttribute('numNotes')) ? this.getAttribute('numNotes') : this._notes.length;

// As in the original VexFlow library: If notesOccupied is not specified,
// default to notesOccupied = 2.
const notesOccupied = this.getAttribute('notesOccupied') || 2;
const location = this._location === 'below' ? -1 : 1;
const bracketed = !this._beamed;

// Following the original VexFlow library:
// If the user specifies whether or not to draw the ratio, respect that.
// If not specified, default to drawing the ratio if the difference between
// num_notes and notes_occupied is greater than 1.
var ratioed;
if (this.hasAttribute('ratioed')) {
ratioed = (this.getAttribute('ratioed') === 'true')
} else {
ratioed = numNotes - notesOccupied > 1;
}

this.tuplet = this._score.tuplet(this._notes,
{
num_notes: numNotes,
notes_occupied: notesOccupied,
location: location,
bracketed: bracketed,
ratioed: ratioed,
}
);

if (this._beamed) {
this.beam = createBeamForNotes(this._score, this._notes);
}

/**
* Tell the parent vf-voice that this vf-tuplet has finished creating its
* notes and beam and is ready to be added to the voice.
*/
this.dispatchEvent(new ElementReadyEvent(ElementReadyEvent.tupletReadyEventName));
}
}

window.customElements.define('vf-tuplet', VFTuplet);
Loading