/*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(); } }); }());