UNPKG

vexflow

Version:

A JavaScript library for rendering music notation and guitar tablature

244 lines (195 loc) 6.71 kB
// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010. // // ## Description // // This file implements the main Voice class. It's mainly a container // object to group `Tickables` for formatting. import { Vex } from './vex'; import { Element } from './element'; import { Flow } from './tables'; import { Fraction } from './fraction'; export class Voice extends Element { // Modes allow the addition of ticks in three different ways: // // STRICT: This is the default. Ticks must fill the voice. // SOFT: Ticks can be added without restrictions. // FULL: Ticks do not need to fill the voice, but can't exceed the maximum // tick length. static get Mode() { return { STRICT: 1, SOFT: 2, FULL: 3, }; } constructor(time) { super(); this.setAttribute('type', 'Voice'); // Time signature shortcut: "4/4", "3/8", etc. if (typeof(time) === 'string') { const match = time.match(/(\d+)\/(\d+)/); if (match) { time = { num_beats: match[1], beat_value: match[2], resolution: Flow.RESOLUTION, }; } } // Default time sig is 4/4 this.time = Vex.Merge({ num_beats: 4, beat_value: 4, resolution: Flow.RESOLUTION, }, time); // Recalculate total ticks. this.totalTicks = new Fraction( this.time.num_beats * (this.time.resolution / this.time.beat_value), 1); this.resolutionMultiplier = 1; // Set defaults this.tickables = []; this.ticksUsed = new Fraction(0, 1); this.smallestTickCount = this.totalTicks.clone(); this.largestTickWidth = 0; this.stave = null; // Do we care about strictly timed notes this.mode = Voice.Mode.STRICT; // This must belong to a VoiceGroup this.voiceGroup = null; } // Get the total ticks in the voice getTotalTicks() { return this.totalTicks; } // Get the total ticks used in the voice by all the tickables getTicksUsed() { return this.ticksUsed; } // Get the largest width of all the tickables getLargestTickWidth() { return this.largestTickWidth; } // Get the tick count for the shortest tickable getSmallestTickCount() { return this.smallestTickCount; } // Get the tickables in the voice getTickables() { return this.tickables; } // Get/set the voice mode, use a value from `Voice.Mode` getMode() { return this.mode; } setMode(mode) { this.mode = mode; return this; } // Get the resolution multiplier for the voice getResolutionMultiplier() { return this.resolutionMultiplier; } // Get the actual tick resolution for the voice getActualResolution() { return this.resolutionMultiplier * this.time.resolution; } // Set the voice's stave setStave(stave) { this.stave = stave; this.boundingBox = null; // Reset bounding box so we can reformat return this; } // Get the bounding box for the voice getBoundingBox() { let stave; let boundingBox; let bb; let i; if (!this.boundingBox) { if (!this.stave) throw new Vex.RERR('NoStave', "Can't get bounding box without stave."); stave = this.stave; boundingBox = null; for (i = 0; i < this.tickables.length; ++i) { this.tickables[i].setStave(stave); bb = this.tickables[i].getBoundingBox(); if (!bb) continue; boundingBox = boundingBox ? boundingBox.mergeWith(bb) : bb; } this.boundingBox = boundingBox; } return this.boundingBox; } // Every tickable must be associated with a voiceGroup. This allows formatters // and preformatters to associate them with the right modifierContexts. getVoiceGroup() { if (!this.voiceGroup) { throw new Vex.RERR('NoVoiceGroup', 'No voice group for voice.'); } return this.voiceGroup; } // Set the voice group setVoiceGroup(g) { this.voiceGroup = g; return this; } // Set the voice mode to strict or soft setStrict(strict) { this.mode = strict ? Voice.Mode.STRICT : Voice.Mode.SOFT; return this; } // Determine if the voice is complete according to the voice mode isComplete() { if (this.mode === Voice.Mode.STRICT || this.mode === Voice.Mode.FULL) { return this.ticksUsed.equals(this.totalTicks); } else { return true; } } // Add a tickable to the voice addTickable(tickable) { if (!tickable.shouldIgnoreTicks()) { const ticks = tickable.getTicks(); // Update the total ticks for this line. this.ticksUsed.add(ticks); if ( (this.mode === Voice.Mode.STRICT || this.mode === Voice.Mode.FULL) && this.ticksUsed.greaterThan(this.totalTicks) ) { this.ticksUsed.subtract(ticks); throw new Vex.RERR('BadArgument', 'Too many ticks.'); } // Track the smallest tickable for formatting. if (ticks.lessThan(this.smallestTickCount)) { this.smallestTickCount = ticks.clone(); } this.resolutionMultiplier = this.ticksUsed.denominator; // Expand total ticks using denominator from ticks used. this.totalTicks.add(0, this.ticksUsed.denominator); } // Add the tickable to the line. this.tickables.push(tickable); tickable.setVoice(this); return this; } // Add an array of tickables to the voice. addTickables(tickables) { for (let i = 0; i < tickables.length; ++i) { this.addTickable(tickables[i]); } return this; } // Preformats the voice by applying the voice's stave to each note. preFormat() { if (this.preFormatted) return this; this.tickables.forEach((tickable) => { if (!tickable.getStave()) { tickable.setStave(this.stave); } }); this.preFormatted = true; return this; } // Render the voice onto the canvas `context` and an optional `stave`. // If `stave` is omitted, it is expected that the notes have staves // already set. draw(context = this.context, stave = this.stave) { this.setRendered(); let boundingBox = null; for (let i = 0; i < this.tickables.length; ++i) { const tickable = this.tickables[i]; // Set the stave if provided if (stave) tickable.setStave(stave); if (!tickable.getStave()) { throw new Vex.RuntimeError( 'MissingStave', 'The voice cannot draw tickables without staves.' ); } if (i === 0) boundingBox = tickable.getBoundingBox(); if (i > 0 && boundingBox) { const tickable_bb = tickable.getBoundingBox(); if (tickable_bb) boundingBox.mergeWith(tickable_bb); } tickable.setContext(context); tickable.draw(); } this.boundingBox = boundingBox; } }