piano/js/components/Key/Key.js

173 lines
4.9 KiB
JavaScript

/*jshint browser:true */
/*globals React, App */
(function () {
'use strict';
/**
* Displays a key on keyboard
*
* @prop {note: int} MIDI note identifier
* @prop {canPlay: function} Whether the note can be manually played
*/
App.components.create('Key', {
displayName: 'Key',
mixins: [React.addons.PureRenderMixin],
propTypes: {
note: React.PropTypes.number.isRequired
},
getInitialState: function () {
return {
channels: []
};
},
getDefaultProps: function () {
return {
note: 0,
canPlay: function () {}
};
},
/**
* Start playing note on given channel with given velocity
*
* @param {channel: number} Channel ID
* @param {velocity: number} Note velocity
*/
on: function (channel, velocity) {
if (this.state.channels.indexOf(channel) > -1) {
return;
}
App.MIDI.output.noteOn(
(channel === -1) ? 0 : channel,
this.props.note,
velocity,
0
);
this.setState({
channels: this.state.channels.concat([
channel
])
});
},
/**
* Stop playing note on previous channel
*
* @param {channel: number} Channel ID
* @param {channel: Array} Channels IDs (default: all current)
*/
off: function (channels) {
var i, length, channel, current,
index;
current = this.state.channels.slice(0);
if (channels === undefined) {
channels = this.state.channels;
} else if (!Array.isArray(channels)) {
channels = [channels];
}
length = channels.length;
for (i = 0; i < length; i += 1) {
channel = channels[i];
App.MIDI.output.noteOff(
(channel === -1) ? 0 : channel,
this.props.note,
0
);
index = current.indexOf(channel);
// remove channels from current playing
if (index > -1) {
current.splice(index, 1);
}
}
this.setState({
channels: current
});
},
/**
* Start playing note on mouse over
*/
mouseOver: function () {
if (!this.props.canPlay()) {
return;
}
this.mouseDown();
},
/**
* Play on mouse down
*/
mouseDown: function (e) {
if (e && e.button !== 0) {
return;
}
this.on(-1, 127);
},
/**
* Render key
*/
render: function () {
var black = App.components.Note.Board.black, offset,
pianoNote = this.props.note, style, channels, channel,
isBlack = (black.indexOf(this.props.note) > -1);
offset = pianoNote - black.filter(function (el) {
return el < pianoNote;
}).length - App.MIDI.keyOffset;
// if it is a black key, we position it absolutely;
// otherwise we just play on its width to avoid rounding
// errors causing display glitches
if (isBlack) {
style = {
left: 'calc(100% / 52 * ' + offset + ')',
zIndex: App.MIDI.keyOffset + 88
};
} else {
style = {
width: 'calc(100% / 52 * ' + (offset + 1) + ')',
zIndex: App.MIDI.keyOffset + 88 - pianoNote
};
}
// get channel ID
channels = this.state.channels;
if (channels.length > 0) {
channel = channels[channels.length - 1];
} else {
channel = false;
}
return React.DOM.span({
className: 'key',
onMouseDown: this.mouseDown,
onMouseOver: this.mouseOver,
onMouseUp: this.off.bind(this, -1),
onMouseOut: this.off.bind(this, -1),
style: style,
'data-channel': channel,
'data-black': isBlack
});
}
});
}());