piano/js/components/Note/Score.js

218 lines
6.3 KiB
JavaScript

/*globals React, Kinetic, App */
(function () {
'use strict';
/**
* Display a set of notes
*
* @prop {notes: array} List of notes
* @prop {time: int} Current playing time
*/
App.components.create('Note', {
displayName: 'Score',
statics: {
channels: [
'hsl(225, 49%, 44%)',
'hsl(22.5, 84%, 56%)',
'hsl(45, 53%, 39%)',
'hsl(67.5, 84%, 56%)',
'hsl(90, 84%, 56%)',
'hsl(112.5, 84%, 56%)',
'hsl(135, 84%, 56%)',
'hsl(157.5, 84%, 56%)',
'hsl(180, 84%, 56%)',
'hsl(202.5, 84%, 56%)',
'hsl(247.5, 84%, 56%)',
'hsl(270, 84%, 56%)',
'hsl(292.5, 84%, 56%)',
'hsl(315, 84%, 56%)',
'hsl(337.5, 84%, 56%)',
'hsl(0, 84%, 56%)'
]
},
propTypes: {
notes: React.PropTypes.array.isRequired,
time: React.PropTypes.number
},
getDefaultProps: function () {
return {
notes: [],
time: 0
};
},
getInitialState: function () {
return {
notes: [],
time: 0,
width: 0,
height: 0
};
},
/**
* When component is mounted, create Kinetic stage and
* attach resizing event
*/
componentDidMount: function () {
var notes;
window.addEventListener('resize', this.resize);
notes = this.props.notes;
this.sort(notes);
this.setState({
notes: notes,
time: this.props.time
}, this.resize);
},
componentWillUnmount: function () {
window.removeEventListener('resize', this.resize);
},
/**
* Redraw canvas on each props update
*/
componentWillReceiveProps: function (nextProps) {
if (nextProps.notes !== this.props.notes) {
this.sort();
}
this.setState({
notes: nextProps.notes,
time: nextProps.time
}, this.draw);
},
/**
* Never rebuild canvas element
*/
shouldComponentUpdate: function (props) {
return false;
},
/**
* Set canvas size
*
* @param {callback: func} Called when size is updated
*/
resize: function (callback) {
var canvas = this.getDOMNode(),
parent = canvas.parentNode,
width = parent.clientWidth,
height = parent.clientHeight,
ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
this.setState({
width: width,
height: height
}, this.draw);
},
/**
* Get note offset by note ID
*
* @param {id: int} Note ID
*/
getOffset: function (id) {
return id - App.MIDI.keyOffset -
App.components.Note.Board.black.filter(function (el) {
return el < id;
}).length;
},
/**
* Get whether a note is black or not
*
* @param {id: int} Note ID
*/
isBlack: function (id) {
return (App.components.Note.Board.black.indexOf(id) > -1);
},
/**
* Sort notes by length
*
* @param {notes: array} Notes list
*/
sort: function (notes) {
var length = notes.length, i;
for (i = 0; i < length; i += 1) {
notes[i].id = i;
}
notes.sort(function (a, b) {
if (a.length === b.length) {
return b.start - a.start;
}
return b.length - a.length;
});
},
/**
* Draw notes
*/
draw: function () {
var notes = this.state.notes, time = this.state.time,
width = this.state.width, height = this.state.height,
xUnit = width / 52, yUnit = xUnit * 5,
maxTime = Math.ceil(height / yUnit),
note, length, i, ctx, offset, x, y, noteWidth, noteHeight,
channels = App.components.Note.Score.channels, count = 0;
ctx = this.getDOMNode().getContext('2d');
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#262626';
ctx.lineWidth = 2;
length = notes.length;
for (i = 0; i < length; i += 1) {
note = notes[i];
if (note.start + note.length > time &&
note.start < time + maxTime) {
offset = this.getOffset(note.note);
count += 1;
x = Math.floor(xUnit * offset);
y = Math.floor(height - yUnit * (note.start + note.length - time));
noteWidth = Math.floor(xUnit);
noteHeight = Math.floor(yUnit * note.length);
if (note.channel === 9 && noteHeight < 10) {
noteHeight = 10;
}
if (this.isBlack(note.note)) {
x -= Math.floor(xUnit / 4);
noteWidth = Math.floor(xUnit / 2);
}
ctx.fillStyle = channels[note.channel];
ctx.strokeRect(x, y, noteWidth, noteHeight);
ctx.fillRect(x, y, noteWidth, noteHeight);
}
}
},
/**
* Render score canvas
*/
render: function () {
return React.DOM.canvas();
}
});
}());