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 basic voice rendering #3

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
15 changes: 10 additions & 5 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ module.exports = (grunt) => {
const BASE_DIR = __dirname;
const BUILD_DIR = path.join(BASE_DIR, 'build');
const RELEASE_DIR = path.join(BASE_DIR, 'releases');
const MODULE_ENTRY = path.join(BASE_DIR, 'src/index.js');
const VEXFLOW_MODULE_ENTRY = path.join(BASE_DIR, 'src/index.js');
const WEB_COMPONENTS_MODULE_ENTRY = path.join(BASE_DIR, 'src/web-components/index.js');
const TARGET_RAW = 'vexflow-debug.js';
const TARGET_MIN = 'vexflow-min.js';

Expand All @@ -31,11 +32,14 @@ module.exports = (grunt) => {
function webpackConfig(target, preset, mode) {
return {
mode,
entry: MODULE_ENTRY,
entry: {
vexflow: VEXFLOW_MODULE_ENTRY,
webComponents: WEB_COMPONENTS_MODULE_ENTRY,
},
output: {
path: BUILD_DIR,
filename: target,
library: 'Vex',
filename: (mode === 'production') ? '[name]-min.js' : '[name]-debug.js',
justinfagnani marked this conversation as resolved.
Show resolved Hide resolved
library: '[name]',
libraryTarget: 'umd',
libraryExport: 'default',
},
Expand All @@ -49,7 +53,8 @@ module.exports = (grunt) => {
loader: 'babel-loader',
options: {
presets: [preset],
plugins: ['@babel/plugin-transform-object-assign'],
plugins: ['@babel/plugin-transform-object-assign',
'@babel/plugin-proposal-class-properties'],
},
}],
},
Expand Down
3,850 changes: 3,466 additions & 384 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"browserify": "^16.5.0",
"canvas": "^2.6.1",
"docco": "^0.8.0",
"es-dev-server": "^1.54.1",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.20.1",
Expand Down Expand Up @@ -52,6 +53,7 @@
"webpack-dev-server": "^3.10.3"
},
"scripts": {
"web-component": "es-dev-server --node-resolve --watch --open /src/web-components/demo/index.html",
"start": "grunt stage",
"lint": "grunt eslint",
"qunit": "grunt test",
Expand Down
21 changes: 21 additions & 0 deletions src/web-components/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<html>
<head>
<title>Vexflow Component</title>
<script type="module" src='../index.js'></script>
</head>
<body>

<vf-score x=10 height=300>
<vf-system>
<vf-stave clef='treble' timeSig='4/4' keySig='Bb'>
<vf-voice autoBeam>C5/q, (C4 Eb4 G4)/q, A4, G4/16, A4, B4, C5</vf-voice>
<vf-voice stem='up'>C#4/h, C#4</vf-voice>
</vf-stave>
<vf-stave clef='bass' timeSig='4/4' keySig='Bb'>
<vf-voice autoBeam>C3/q, (C3 Eb3 G3)/q, A2/q, B2</vf-voice>
</vf-stave>
</vf-system>
</vf-score>

</body>
</html>
4 changes: 4 additions & 0 deletions src/web-components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { VFScore } from './vf-score';
export { VFSystem } from './vf-system';
export { VFStave } from './vf-stave';
export { VFVoice } from './vf-voice';
203 changes: 203 additions & 0 deletions src/web-components/vf-score.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// ## Description
//
// This file implements `vf-score`, the web component that acts as the
// container for the entire component. `vf-score`'s shadow root holds
// the HTML element that the renderer renders too.
//
// All actual drawing is called from `vf-score`.

import Vex from '../index';

export class VFScore extends HTMLElement {

/**
* The starting x position of a system within the score.
* @private
*/
_x = 10;

/**
* The starting y position of system within the score.
* @private
*/
_y = 0;

/**
* The entire width of the element holding the music notation.
* @private
*/
_width = 500;

/**
* The entire height of the element holding the music notation.
* @private
*/
_height = 150;

/**
* The number of systems per line.
* @private
*/
_systemsPerLine = 1;

/**
* Counter that keeps track of how many systems have dispatched events
* signalling that they are ready to be drawn. When the number of children
* matches this counter, the entire score is ready to be drawn.
* FOR THIS PR: a vf-score can only have one vf-system.
* @private
*/
_systemsAdded = 0;

/**
* The type of renderer to use for this vf-score component.
* @private
*/
_rendererType = 'svg';

constructor() {
super();

this.attachShadow({ mode:'open' });

// The 'systemCreated' event is dispatched by a vf-systen when it has
// finished creating and adding its staves. vf-score listens to this event
// so that it can add that it can detect when the vf-system is ready to
// be drawn.
this.addEventListener('systemCreated', this.systemCreated);

// These events are dispatched by the corresponding elements
// (ex: vfVoiceReady is dispatched by a vf-voice) when they are added to the
// DOM. vf-score listens to these events so that it can set the elements'
// Factory and/or Registry instances, since these are shared by a vf-score
// and all its children to maintain the same render queue.
this.addEventListener('vfVoiceReady', this.setFactory);
this.addEventListener('vfStaveReady', this.setFactory);
this.addEventListener('vfStaveReady', this.setRegistry);
this.addEventListener('vfSystemReady', this.setFactory);

this._setupVexflow();
this._setupFactory();
}

connectedCallback() {
// vf-score listens to the slotchange event so that it can detect its system
// and set it up accordingly
this.shadowRoot.querySelector('slot').addEventListener('slotchange', this.registerSystem);
}

disconnectedCallback() {
this.shadowRoot.querySelector('slot').removeEventListener('slotchange', this.registerSystem);
ywsang marked this conversation as resolved.
Show resolved Hide resolved
}

static get observedAttributes() { return ['x', 'y', 'width', 'height', 'renderer'] }

attributeChangedCallback(name, oldValue, newValue) {
switch(name) {
case 'x':
case 'y':
// TODO (ywsang): Implement code to update the position of the vf-system
// children.
break;
case 'width':
this._width = parseInt(newValue);
this.resizeRenderer();
break;
case 'height':
this._height = parseInt(newValue);
this.resizeRenderer();
break;
case 'renderer':
this._rendererType = newValue;
break;
}
}

resizeRenderer() {
this.renderer.resize(this._width, this._height);
}

/**
* Sets up the renderer, context, and registry for this component.
* @private
*/
_setupVexflow() {
// Default to the SVG renderer if not specified.
this.shadowRoot.innerHTML = this.rendererType === 'canvas'
? `<canvas id='vf-score'><slot></slot></canvas>`
: `<div id='vf-score'><slot></slot></div>`
const element = this.shadowRoot.querySelector('#vf-score')

if (this._rendererType === 'canvas') {
this.renderer = new Vex.Flow.Renderer(element, Vex.Flow.Renderer.Backends.CANVAS);
} else {
this.renderer = new Vex.Flow.Renderer(element, Vex.Flow.Renderer.Backends.SVG);
}

this.renderer.resize(this._width, this._height);
this.context = this.renderer.getContext();
this.registry = new Vex.Flow.Registry();
ywsang marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @private
*/
_setupFactory() {
// Factory is constructed with a null elementId because the underlying code
// uses document.querySelector(elementId) to find the element to attach the
// renderer to, but the web component puts the element in the shadow DOM. As
// such, the element would not be found due to DOM encapsulation. However,
// in order to use the simplified EasyScore API constructors, a Factory
// instance is still needed.
this.vf = new Vex.Flow.Factory({ renderer: { elementId: null } });
this.vf.setContext(this.context);
}

getNoteFromId(id) {
return this.registry.getElementById(id);
}

/**
* "Registers" the vf-system child.
* This PR only supports/assumes one vf-system per vf-score.
*/
registerSystem = () => {
const system = this.shadowRoot.querySelector('slot').assignedElements()[0];
// TODO (ywsang): Figure out how to account for any added connectors that
// get drawn in front of the x position (e.g. brace, bracket)
system.setupSystem(this._x, this._y, this._width - this._x - 1); // Minus 1 to account for the overflow of the right bar line
}

/**
* Once all systems have dispatched events signalling that they've added their
* staves, the entire score is drawn.
*/
systemCreated = () => {
this.addSystemConnectors();
this.vf.draw();
}

addSystemConnectors() {
const system = this.vf.systems[0]; // TODO (ywsang): Replace with better
// logic once more than one system per
// score is allowed.
system.addConnector('singleRight');
system.addConnector('singleLeft');
}

/**
* Sets the factory instance of the component that dispatched the event.
*/
setFactory = (event) => {
event.target.vf = this.vf;
}

/**
* Sets the registry instance of the component that dispatched the event.
*/
setRegistry = (event) => {
event.target.registry = this.registry;
}
}

window.customElements.define('vf-score', VFScore);
Loading