172 lines
4.8 KiB
JavaScript
172 lines
4.8 KiB
JavaScript
/*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
|
|
});
|
|
}
|
|
});
|
|
}()); |