From c96a18efceffaddf32462d7b79ea843dc47a0033 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 26 Nov 2015 10:15:39 -0200 Subject: [PATCH 01/46] initial commit --- src/editor.js | 220 ++++++++++++++++++++++++++++++++++++++++++++++++ src/songdata.js | 173 +++++++++++++++++++++++++++++++++++++ test.html | 17 ++++ test.js | 17 ++++ 4 files changed, 427 insertions(+) create mode 100644 src/editor.js create mode 100644 src/songdata.js create mode 100644 test.html create mode 100644 test.js diff --git a/src/editor.js b/src/editor.js new file mode 100644 index 0000000..5f35dc7 --- /dev/null +++ b/src/editor.js @@ -0,0 +1,220 @@ +function SongEditor(canvas, songData) +{ + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); + this.canvasWidth = parseFloat(canvas.width); + this.canvasHeight = parseFloat(canvas.height); + + this.songData = songData; + + this.noteSelections = []; + this.chordSelections = []; + this.keyChangeSelections = []; + this.meterChangeSelections = []; + + this.viewBlocks = []; + this.viewNotes = []; + this.viewChords = []; + this.viewKeyChanges = []; + this.viewMeterChanges = []; + this.tickZoom = 1; + + this.MARGIN_LEFT = 4; + this.MARGIN_RIGHT = 4; + this.MARGIN_TOP = 4; + this.MARGIN_BOTTOM = 4; + this.HEADER_MARGIN = 40; + this.CHORD_HEIGHT = 60; + this.CHORDNOTE_MARGIN = 10; + this.KEYCHANGE_BAR_WIDTH = 10; + this.METERCHANGE_BAR_WIDTH = 10; + + this.refreshVisualization(); +} + + +SongEditor.prototype.setData = function(songData) +{ + this.songData = songData; +} + + +SongEditor.prototype.refreshVisualization = function() +{ + this.viewBlocks = []; + this.viewNotes = []; + this.viewChords = []; + this.viewKeyChanges = []; + this.viewMeterChanges = []; + + var blockY1 = this.MARGIN_TOP + this.HEADER_MARGIN; + var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + var changeY1 = this.MARGIN_TOP; + var chordY2 = this.canvasHeight - this.MARGIN_BOTTOM; + + var x = this.MARGIN_LEFT; + var tick = 0; + var curNote = 0; + var curChord = 0; + var curKeyChange = 0; + var curMeterChange = 0; + + var curBlock = 0; + this.viewBlocks.push( + { + tick: 0, + duration: 0, + key: null, + meter: null, + x1: x, + y1: blockY1, + x2: x, + y2: blockY2 + }); + + var NEXT_IS_NONE = 0; + var NEXT_IS_KEYCHANGE = 1; + var NEXT_IS_METERCHANGE = 2; + + while (true) + { + // Find the tick where the current block ends. + var nextChangeTick = this.songData.lastTick; + var nextIsWhat = NEXT_IS_NONE; + + if (curKeyChange < this.songData.keyChanges.length) + { + var nextKeyChange = this.songData.keyChanges[curKeyChange] + if (nextKeyChange.tick < nextChangeTick) + { + nextChangeTick = nextKeyChange.tick; + nextIsWhat = NEXT_IS_KEYCHANGE; + } + } + + if (curMeterChange < this.songData.meterChanges.length) + { + var nextMeterChange = this.songData.meterChanges[curMeterChange] + if (nextMeterChange.tick < nextChangeTick) + { + nextChangeTick = nextMeterChange.tick; + nextIsWhat = NEXT_IS_METERCHANGE; + } + } + + // Advance draw position until the next tick. + x += (nextChangeTick - tick) * this.tickZoom; + tick = nextChangeTick; + + // If there is a key change, add its visualization and advance its iterator. + var blockX2 = x; + + if (nextIsWhat == NEXT_IS_KEYCHANGE) + { + x += this.KEYCHANGE_BAR_WIDTH; + + this.viewKeyChanges.push( + { + keyChange: this.songData.keyChanges[curKeyChange], + tick: nextChangeTick, + x1: blockX2, + y1: changeY1, + x2: x, + y2: chordY2 + }); + + curKeyChange++; + } + // Or if there is a meter change, add its visualization and advance its iterator. + else if (nextIsWhat == NEXT_IS_METERCHANGE) + { + x += this.METERCHANGE_BAR_WIDTH; + + this.viewMeterChanges.push( + { + meterChange: this.songData.meterChanges[curMeterChange], + tick: nextChangeTick, + x1: blockX2, + y1: changeY1, + x2: x, + y2: chordY2 + }); + + curMeterChange++; + } + + // Then finish off the current block of notes. + // If its duration would be zero, ignore it for now but prepare it for the next iteration. + var block = this.viewBlocks[curBlock]; + + if (nextChangeTick == block.tick) + { + block.x1 = x; + block.x2 = x; + + if (nextIsWhat == NEXT_IS_KEYCHANGE) + block.key = this.songData.keyChanges[curKeyChange - 1]; + else if (nextIsWhat == NEXT_IS_METERCHANGE) + block.meter = this.songData.meterChanges[curMeterChange - 1]; + } + else + { + block.duration = nextChangeTick - block.tick; + block.x2 = blockX2; + + // If this is the final block, we can stop now. + if (nextIsWhat == NEXT_IS_NONE) + break; + + // Or else, add a new block for the next iteration. + this.viewBlocks.push( + { + tick: tick, + duration: 0, + key: block.key, + meter: block.meter, + x1: x, + y1: blockY1, + x2: x, + y2: blockY2 + }); + + curBlock++; + } + } +} + + +SongEditor.prototype.refreshCanvas = function() +{ + this.ctx.fillStyle = "white"; + this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); + + for (var i = 0; i < this.viewBlocks.length; i++) + { + var block = this.viewBlocks[i]; + + this.ctx.strokeStyle = "black"; + this.ctx.lineWidth = 2; + + var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); + this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - block.y1); + this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); + } + + for (var i = 0; i < this.viewKeyChanges.length; i++) + { + var keyChange = this.viewKeyChanges[i]; + + this.ctx.fillStyle = "red"; + this.ctx.fillRect((keyChange.x1 + keyChange.x2) / 2 - 1, keyChange.y1, 2, keyChange.y2 - keyChange.y1); + } + + for (var i = 0; i < this.viewMeterChanges.length; i++) + { + var meterChange = this.viewMeterChanges[i]; + + this.ctx.fillStyle = "blue"; + this.ctx.fillRect((meterChange.x1 + meterChange.x2) / 2 - 1, meterChange.y1, 2, meterChange.y2 - meterChange.y1); + } +} \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js new file mode 100644 index 0000000..b21cf42 --- /dev/null +++ b/src/songdata.js @@ -0,0 +1,173 @@ +function SongData() +{ + this.beatsPerMinute = 120; + this.ticksPerBeat = 120; + this.lastTick = 1000; + this.notes = []; + this.chords = []; + this.keyChanges = []; + this.meterChanges = []; +} + + +function SongDataNote(tick, duration, pitch) +{ + this.tick = tick; + this.duration = duration; + this.pitch = pitch; +} + + +function SongDataKeyChange(tick, key, tonicPitch) +{ + this.tick = tick; + this.key = key; + this.tonicPitch = tonicPitch; +} + + +function SongDataMeterChange(tick, numerator, denominator) +{ + this.tick = tick; + this.numerator = numerator; + this.denominator = denominator; +} + + +function arrayAddSortedByTick(arr, obj) +{ + // TODO: Use binary search to avoid iterating through the entire array. + for (var i = 0; i < arr.length; i++) + { + var otherObj = arr[i]; + + if (otherObj.tick > obj.tick) + { + arr.splice(i, 0, obj); + return; + } + } + + arr.push(obj); +} + + +// Returns whether the given tick value is valid. +SongData.prototype.isValidTick = function(tick) +{ + // Arbitrary upper limit. + return (tick >= 0 && tick < 1000000); +} + + +// Returns whether the given duration value is valid. +SongData.prototype.isValidDuration = function(duration) +{ + // Arbitrary upper limit. + return (duration > 0 && duration < 10000); +} + + +// Returns whether it is valid for the given note to be added to the data. +SongData.prototype.canAddNote = function(note) +{ + // Check for invalid values. + if (!this.isValidTick(note.tick) || + !this.isValidDuration(note.duration)) + return false; + + // Check whether the given note collides with any notes already in data. + // TODO: Use binary search to avoid iterating through the entire array. + for (var i = 0; i < this.notes.length; i++) + { + var otherNote = this.notes[i]; + + if (otherNote.pitch == note.pitch && + otherNote.tick < note.tick + note.duration && + otherNote.tick + otherNote.duration > note.tick) + return false; + } + + return true; +} + + +// Returns whether it is valid for the given key change to be added to the data. +SongData.prototype.canAddKeyChange = function(keyChange) +{ + // Check for invalid values. + if (!this.isValidTick(keyChange.tick)) + return false; + + // Check whether the given key change coincides with another already in data. + // TODO: Use binary search to avoid iterating through the entire array. + for (var i = 0; i < this.keyChanges.length; i++) + { + var otherKeyChange = this.keyChanges[i]; + + if (otherKeyChange.tick == keyChange.tick) + return false; + } + + return true; +} + + +// Returns whether it is valid for the given meter change to be added to the data. +SongData.prototype.canAddMeterChange = function(meterChange) +{ + // Check for invalid values. + if (!this.isValidTick(meterChange.tick)) + return false; + + // Check whether the given meter change coincides with another already in data. + // TODO: Use binary search to avoid iterating through the entire array. + for (var i = 0; i < this.meterChanges.length; i++) + { + var otherMeterChange = this.meterChanges[i]; + + if (otherMeterChange.tick == meterChange.tick) + return false; + } + + return true; +} + + +// Adds the given note to the data, and returns whether it was successful. +SongData.prototype.addNote = function(note) +{ + if (this.canAddNote(note)) + { + arrayAddSortedByTick(this.notes, note); + return true; + } + + return false; +} + + +// Adds the given key change to the data, and returns whether it was successful. +SongData.prototype.addKeyChange = function(keyChange) +{ + if (this.canAddKeyChange(keyChange)) + { + arrayAddSortedByTick(this.keyChanges, keyChange); + return true; + } + + return false; +} + + +// Adds the given meter change to the data, and returns whether it was successful. +SongData.prototype.addMeterChange = function(meterChange) +{ + if (this.canAddMeterChange(meterChange)) + { + arrayAddSortedByTick(this.meterChanges, meterChange); + return true; + } + + return false; +} \ No newline at end of file diff --git a/test.html b/test.html new file mode 100644 index 0000000..b52d5f8 --- /dev/null +++ b/test.html @@ -0,0 +1,17 @@ + + + + + + Test + + + + + + + + + + + \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..3e01e95 --- /dev/null +++ b/test.js @@ -0,0 +1,17 @@ +function testOnLoad() +{ + var song = new SongData(); + song.addKeyChange(new SongDataKeyChange(0, 0, 0)); + song.addKeyChange(new SongDataKeyChange(100, 0, 0)); + song.addMeterChange(new SongDataMeterChange(150, 0, 0)); + song.addKeyChange(new SongDataKeyChange(220, 0, 0)); + song.addKeyChange(new SongDataKeyChange(250, 0, 0)); + song.addMeterChange(new SongDataMeterChange(250, 0, 0)); + song.addKeyChange(new SongDataKeyChange(251, 0, 0)); + song.addMeterChange(new SongDataMeterChange(251, 0, 0)); + song.addKeyChange(new SongDataKeyChange(350, 0, 0)); + + var canvas = document.getElementById("editorCanvas"); + var editor = new SongEditor(canvas, song); + editor.refreshCanvas(); +} \ No newline at end of file From 291f9cf2e98f2bdfc47f31a1b2ca55b98614fb20 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 26 Nov 2015 13:37:42 -0200 Subject: [PATCH 02/46] start note drawing --- src/editor.js | 69 +++++++++++++++++++++++++++++++++++++++++++++---- src/songdata.js | 24 +++++++++++++++-- test.js | 18 ++++++------- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src/editor.js b/src/editor.js index 5f35dc7..74a4e60 100644 --- a/src/editor.js +++ b/src/editor.js @@ -24,6 +24,7 @@ function SongEditor(canvas, songData) this.MARGIN_TOP = 4; this.MARGIN_BOTTOM = 4; this.HEADER_MARGIN = 40; + this.NOTE_HEIGHT = 10; this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; this.KEYCHANGE_BAR_WIDTH = 10; @@ -151,11 +152,6 @@ SongEditor.prototype.refreshVisualization = function() { block.x1 = x; block.x2 = x; - - if (nextIsWhat == NEXT_IS_KEYCHANGE) - block.key = this.songData.keyChanges[curKeyChange - 1]; - else if (nextIsWhat == NEXT_IS_METERCHANGE) - block.meter = this.songData.meterChanges[curMeterChange - 1]; } else { @@ -181,7 +177,63 @@ SongEditor.prototype.refreshVisualization = function() curBlock++; } + + // Apply key/meter changes to the following block. + if (nextIsWhat == NEXT_IS_KEYCHANGE) + this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; + else if (nextIsWhat == NEXT_IS_METERCHANGE) + this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; + } +} + + +// Returns the row number where a note of the given pitch would be placed, +// according to the given scale. +SongEditor.prototype.getPitchRow = function(pitch, scale) +{ + // FIXME: This returns test values. + return pitch / 2 + (scale.name == "Dorian" ? 0 : 1); +} + + +SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration) +{ + var block = this.viewBlocks[blockIndex]; + + if (tick + duration <= block.tick || + tick >= block.tick + block.duration) + return; + + var row = this.getPitchRow(pitch, block.key.scale); + + var blockTick = tick - block.tick; + var x1 = block.x1 + Math.max(0, blockTick * this.tickZoom); + var x2 = block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom); + var y1 = block.y2 - (row + 1) * this.NOTE_HEIGHT; + var y2 = y1 + this.NOTE_HEIGHT; + + this.ctx.save(); + this.ctx.fillStyle = "red"; + this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); + this.ctx.globalAlpha = 0.5; + + if (blockTick + duration > block.duration && blockIndex < this.viewBlocks.length - 1) + { + var nextBlock = this.viewBlocks[blockIndex + 1]; + var nextRow = this.getPitchRow(pitch, nextBlock.key.scale); + + var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; + var nextY2 = nextY1 + this.NOTE_HEIGHT; + + this.ctx.beginPath(); + this.ctx.moveTo(block.x2, y1); + this.ctx.lineTo(nextBlock.x1, nextY1); + this.ctx.lineTo(nextBlock.x1, nextY2); + this.ctx.lineTo(block.x2, y2); + this.ctx.fill(); } + + this.ctx.restore(); } @@ -192,6 +244,12 @@ SongEditor.prototype.refreshCanvas = function() for (var i = 0; i < this.viewBlocks.length; i++) { + for (var n = 0; n < this.songData.notes.length; n++) + { + var note = this.songData.notes[n]; + this.drawNote(i, note.pitch, note.tick, note.duration); + } + var block = this.viewBlocks[i]; this.ctx.strokeStyle = "black"; @@ -200,6 +258,7 @@ SongEditor.prototype.refreshCanvas = function() var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - block.y1); this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); + } for (var i = 0; i < this.viewKeyChanges.length; i++) diff --git a/src/songdata.js b/src/songdata.js index b21cf42..bc6f180 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -1,3 +1,23 @@ +var C = 0; +var Cs = 1; +var D = 2; +var Ds = 3; +var E = 4; +var F = 5; +var Fs = 6; +var G = 7; +var Gs = 8; +var A = 9; +var As = 10; +var B = 11; + +var scaleMajor = { name: "Major", pitches: [ C, D, E, F, G, A, B ] }; +var scaleDorian = { name: "Dorian", pitches: [ C, D, Ds, F, G, A, As ] }; +var scaleMixolydian = { name: "Mixolydian", pitches: [ C, D, E, F, G, A, As ] }; +var scaleNaturalMinor = { name: "Natural Minor", pitches: [ C, D, Ds, F, G, Gs, As ] }; +var scalePhrygianDominant = { name: "Phrygian Dominant", pitches: [ C, Cs, E, F, G, Gs, As ] }; + + function SongData() { this.beatsPerMinute = 120; @@ -18,10 +38,10 @@ function SongDataNote(tick, duration, pitch) } -function SongDataKeyChange(tick, key, tonicPitch) +function SongDataKeyChange(tick, scale, tonicPitch) { this.tick = tick; - this.key = key; + this.scale = scale; this.tonicPitch = tonicPitch; } diff --git a/test.js b/test.js index 3e01e95..95fc40b 100644 --- a/test.js +++ b/test.js @@ -1,15 +1,15 @@ function testOnLoad() { var song = new SongData(); - song.addKeyChange(new SongDataKeyChange(0, 0, 0)); - song.addKeyChange(new SongDataKeyChange(100, 0, 0)); - song.addMeterChange(new SongDataMeterChange(150, 0, 0)); - song.addKeyChange(new SongDataKeyChange(220, 0, 0)); - song.addKeyChange(new SongDataKeyChange(250, 0, 0)); - song.addMeterChange(new SongDataMeterChange(250, 0, 0)); - song.addKeyChange(new SongDataKeyChange(251, 0, 0)); - song.addMeterChange(new SongDataMeterChange(251, 0, 0)); - song.addKeyChange(new SongDataKeyChange(350, 0, 0)); + song.addKeyChange(new SongDataKeyChange(0, scaleMajor, 0)); + song.addKeyChange(new SongDataKeyChange(100, scaleDorian, 0)); + song.addKeyChange(new SongDataKeyChange(220, scaleMajor, 0)); + + song.addNote(new SongDataNote(0, 25, 0)); + song.addNote(new SongDataNote(25, 25, 1)); + song.addNote(new SongDataNote(50, 25, 2)); + song.addNote(new SongDataNote(75, 25, 3)); + song.addNote(new SongDataNote(0, 300, 8)); var canvas = document.getElementById("editorCanvas"); var editor = new SongEditor(canvas, song); From c3940c8d1bff1f91a94dcde5703bd92d54bd1bc6 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 26 Nov 2015 15:02:41 -0200 Subject: [PATCH 03/46] add note mouse hovering --- src/editor.js | 180 +++++++++++++++++++++++++++++++++++++++++------- src/songdata.js | 10 +-- test.js | 9 ++- 3 files changed, 164 insertions(+), 35 deletions(-) diff --git a/src/editor.js b/src/editor.js index 74a4e60..1f89b28 100644 --- a/src/editor.js +++ b/src/editor.js @@ -5,6 +5,9 @@ function SongEditor(canvas, songData) this.canvasWidth = parseFloat(canvas.width); this.canvasHeight = parseFloat(canvas.height); + var that = this; + this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; + this.songData = songData; this.noteSelections = []; @@ -12,19 +15,20 @@ function SongEditor(canvas, songData) this.keyChangeSelections = []; this.meterChangeSelections = []; + this.tickZoom = 1; this.viewBlocks = []; this.viewNotes = []; this.viewChords = []; this.viewKeyChanges = []; this.viewMeterChanges = []; - this.tickZoom = 1; + this.hoverNote = -1; this.MARGIN_LEFT = 4; this.MARGIN_RIGHT = 4; this.MARGIN_TOP = 4; this.MARGIN_BOTTOM = 4; this.HEADER_MARGIN = 40; - this.NOTE_HEIGHT = 10; + this.NOTE_HEIGHT = 14; this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; this.KEYCHANGE_BAR_WIDTH = 10; @@ -40,6 +44,56 @@ SongEditor.prototype.setData = function(songData) } +function isPointInside(p, rect) +{ + return ( + p.x >= rect.x1 && p.x <= rect.x2 && + p.y >= rect.y1 && p.y <= rect.y2); +} + + +function transformMousePosition(elem, ev) +{ + var rect = elem.getBoundingClientRect(); + return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; +} + + +SongEditor.prototype.handleMouseMove = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + this.canvas.style.cursor = "default"; + + this.hoverNote = -1; + for (var b = 0; b < this.viewBlocks.length; b++) + { + if (isPointInside(mousePos, this.viewBlocks[b])) + { + for (var n = 0; n < this.viewBlocks[b].notes.length; n++) + { + var note = this.viewBlocks[b].notes[n]; + if (isPointInside(mousePos, note)) + { + this.hoverNote = note.noteIndex; + + if (mousePos.x <= note.trueX1 + 2 || mousePos.x >= note.trueX2 - 2) + this.canvas.style.cursor = "ew-resize"; + else + this.canvas.style.cursor = "pointer"; + + break; + } + } + break; + } + } + + this.refreshCanvas(); +} + + SongEditor.prototype.refreshVisualization = function() { this.viewBlocks = []; @@ -48,6 +102,10 @@ SongEditor.prototype.refreshVisualization = function() this.viewKeyChanges = []; this.viewMeterChanges = []; + this.noteSelections = []; + for (var i = 0; i < this.songData.notes.length; i++) + this.noteSelections.push(false); + var blockY1 = this.MARGIN_TOP + this.HEADER_MARGIN; var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; var changeY1 = this.MARGIN_TOP; @@ -67,6 +125,7 @@ SongEditor.prototype.refreshVisualization = function() duration: 0, key: null, meter: null, + notes: [], x1: x, y1: blockY1, x2: x, @@ -158,6 +217,25 @@ SongEditor.prototype.refreshVisualization = function() block.duration = nextChangeTick - block.tick; block.x2 = blockX2; + for (var n = 0; n < this.songData.notes.length; n++) + { + var note = this.songData.notes[n]; + var noteRow = this.getNoteRow(note.pitch, block.key.scale); + var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); + block.notes.push( + { + noteIndex: n, + tick: note.tick, + duration: note.duration, + trueX1: notePos.trueX1, + trueX2: notePos.trueX2, + x1: notePos.x1, + y1: notePos.y1, + x2: notePos.x2, + y2: notePos.y2 + }); + } + // If this is the final block, we can stop now. if (nextIsWhat == NEXT_IS_NONE) break; @@ -169,6 +247,7 @@ SongEditor.prototype.refreshVisualization = function() duration: 0, key: block.key, meter: block.meter, + notes: [], x1: x, y1: blockY1, x2: x, @@ -189,14 +268,61 @@ SongEditor.prototype.refreshVisualization = function() // Returns the row number where a note of the given pitch would be placed, // according to the given scale. -SongEditor.prototype.getPitchRow = function(pitch, scale) +SongEditor.prototype.getNoteRow = function(pitch, scale) +{ + var pitchInOctave = (pitch % 12); + var pitchDegree = 0; + + for (var i = 0; i < scale.degrees.length; i++) + { + if (scale.degrees[i] == pitchInOctave) + { + pitchDegree = i; + break; + } + else if (scale.degrees[i] > pitchInOctave) + { + pitchDegree = i - 0.5; + break; + } + } + + return pitchDegree; +} + + +// Returns a color string matching the given row. +SongEditor.prototype.getColorForRow = function(row) +{ + var colors = + [ + "#ff0000", + "#ff8800", + "#eeee00", + "#44dd33", + "#0000ff", + "#8800ff", + "#ff00ff" + ]; + return colors[row - (row % 1)]; +} + + +SongEditor.prototype.getNotePosition = function(block, row, tick, duration) { - // FIXME: This returns test values. - return pitch / 2 + (scale.name == "Dorian" ? 0 : 1); + var blockTick = tick - block.tick; + return { + trueX1: block.x1 + blockTick * this.tickZoom, + trueX2: block.x1 + (blockTick + duration) * this.tickZoom, + x1: block.x1 + Math.max(0, blockTick * this.tickZoom), + x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom), + y1: block.y2 - (row + 1) * this.NOTE_HEIGHT, + y2: block.y2 - (row) * this.NOTE_HEIGHT, + }; } -SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration) +SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hovering, selected) { var block = this.viewBlocks[blockIndex]; @@ -204,32 +330,37 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration) tick >= block.tick + block.duration) return; - var row = this.getPitchRow(pitch, block.key.scale); - - var blockTick = tick - block.tick; - var x1 = block.x1 + Math.max(0, blockTick * this.tickZoom); - var x2 = block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom); - var y1 = block.y2 - (row + 1) * this.NOTE_HEIGHT; - var y2 = y1 + this.NOTE_HEIGHT; + var row = this.getNoteRow(pitch, block.key.scale); + var pos = this.getNotePosition(block, row, tick, duration); + var col = this.getColorForRow(row); this.ctx.save(); - this.ctx.fillStyle = "red"; - this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); + this.ctx.fillStyle = col; + if (hovering) + { + this.ctx.globalAlpha = 0.5; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, 3); + this.ctx.fillRect(pos.x1, pos.y2 - 3, pos.x2 - pos.x1, 3); + } + else + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.globalAlpha = 0.5; - if (blockTick + duration > block.duration && blockIndex < this.viewBlocks.length - 1) + if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) { var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = this.getPitchRow(pitch, nextBlock.key.scale); + var nextRow = this.getNoteRow(pitch, nextBlock.key.scale); var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; var nextY2 = nextY1 + this.NOTE_HEIGHT; this.ctx.beginPath(); - this.ctx.moveTo(block.x2, y1); + this.ctx.moveTo(block.x2, pos.y1); this.ctx.lineTo(nextBlock.x1, nextY1); this.ctx.lineTo(nextBlock.x1, nextY2); - this.ctx.lineTo(block.x2, y2); + this.ctx.lineTo(block.x2, pos.y2); this.ctx.fill(); } @@ -244,21 +375,20 @@ SongEditor.prototype.refreshCanvas = function() for (var i = 0; i < this.viewBlocks.length; i++) { - for (var n = 0; n < this.songData.notes.length; n++) + var block = this.viewBlocks[i]; + + for (var n = 0; n < block.notes.length; n++) { - var note = this.songData.notes[n]; - this.drawNote(i, note.pitch, note.tick, note.duration); + var note = this.songData.notes[block.notes[n].noteIndex]; + this.drawNote(i, note.pitch, note.tick, note.duration, block.notes[n].noteIndex == this.hoverNote, false); } - var block = this.viewBlocks[i]; - this.ctx.strokeStyle = "black"; this.ctx.lineWidth = 2; var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - block.y1); this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); - } for (var i = 0; i < this.viewKeyChanges.length; i++) diff --git a/src/songdata.js b/src/songdata.js index bc6f180..248df13 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -11,11 +11,11 @@ var A = 9; var As = 10; var B = 11; -var scaleMajor = { name: "Major", pitches: [ C, D, E, F, G, A, B ] }; -var scaleDorian = { name: "Dorian", pitches: [ C, D, Ds, F, G, A, As ] }; -var scaleMixolydian = { name: "Mixolydian", pitches: [ C, D, E, F, G, A, As ] }; -var scaleNaturalMinor = { name: "Natural Minor", pitches: [ C, D, Ds, F, G, Gs, As ] }; -var scalePhrygianDominant = { name: "Phrygian Dominant", pitches: [ C, Cs, E, F, G, Gs, As ] }; +var scaleMajor = { name: "Major", degrees: [ C, D, E, F, G, A, B ] }; +var scaleDorian = { name: "Dorian", degrees: [ C, D, Ds, F, G, A, As ] }; +var scaleMixolydian = { name: "Mixolydian", degrees: [ C, D, E, F, G, A, As ] }; +var scaleNaturalMinor = { name: "Natural Minor", degrees: [ C, D, Ds, F, G, Gs, As ] }; +var scalePhrygianDominant = { name: "Phrygian Dominant", degrees: [ C, Cs, E, F, G, Gs, As ] }; function SongData() diff --git a/test.js b/test.js index 95fc40b..4d4c1e6 100644 --- a/test.js +++ b/test.js @@ -3,12 +3,11 @@ function testOnLoad() var song = new SongData(); song.addKeyChange(new SongDataKeyChange(0, scaleMajor, 0)); song.addKeyChange(new SongDataKeyChange(100, scaleDorian, 0)); - song.addKeyChange(new SongDataKeyChange(220, scaleMajor, 0)); + song.addKeyChange(new SongDataKeyChange(200, scaleMajor, 0)); + song.addKeyChange(new SongDataKeyChange(300, scaleMajor, 0)); - song.addNote(new SongDataNote(0, 25, 0)); - song.addNote(new SongDataNote(25, 25, 1)); - song.addNote(new SongDataNote(50, 25, 2)); - song.addNote(new SongDataNote(75, 25, 3)); + song.addNote(new SongDataNote(0, 300, 0)); + song.addNote(new SongDataNote(0, 300, 3)); song.addNote(new SongDataNote(0, 300, 8)); var canvas = document.getElementById("editorCanvas"); From eb46d8ec18ef9c78facc9b62d29d74c3afbe7600 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 26 Nov 2015 22:14:31 -0200 Subject: [PATCH 04/46] break editor into multiple files; add interaction up to moving key/meter changes --- src/editor.js | 384 ++------------------------------- src/editor_drawing.js | 197 +++++++++++++++++ src/editor_helpers.js | 55 +++++ src/editor_interaction.js | 398 +++++++++++++++++++++++++++++++++++ src/editor_representation.js | 180 ++++++++++++++++ src/theory.js | 5 + test.html | 5 + test.js | 20 +- 8 files changed, 874 insertions(+), 370 deletions(-) create mode 100644 src/editor_drawing.js create mode 100644 src/editor_helpers.js create mode 100644 src/editor_interaction.js create mode 100644 src/editor_representation.js create mode 100644 src/theory.js diff --git a/src/editor.js b/src/editor.js index 1f89b28..cc43add 100644 --- a/src/editor.js +++ b/src/editor.js @@ -7,6 +7,8 @@ function SongEditor(canvas, songData) var that = this; this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; + this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; + this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; this.songData = songData; @@ -16,12 +18,23 @@ function SongEditor(canvas, songData) this.meterChangeSelections = []; this.tickZoom = 1; + this.tickSnap = 5; this.viewBlocks = []; this.viewNotes = []; this.viewChords = []; this.viewKeyChanges = []; this.viewMeterChanges = []; + this.mouseDown = false; + this.mouseDragAction = null; + this.mouseDragCurrent = { x: 0, y: 0 }; + this.mouseDragOrigin = { x: 0, y: 0 }; + this.mouseStretchPivotTick = 0; + this.mouseStretchOriginTick = 0; this.hoverNote = -1; + this.hoverKeyChange = -1; + this.hoverMeterChange = -1; + this.hoverStretchR = false; + this.hoverStretchL = false; this.MARGIN_LEFT = 4; this.MARGIN_RIGHT = 4; @@ -29,381 +42,18 @@ function SongEditor(canvas, songData) this.MARGIN_BOTTOM = 4; this.HEADER_MARGIN = 40; this.NOTE_HEIGHT = 14; + this.NOTE_MARGIN_HOR = 0.5; + this.NOTE_MARGIN_VER = 0.5; this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; this.KEYCHANGE_BAR_WIDTH = 10; - this.METERCHANGE_BAR_WIDTH = 10; + this.METERCHANGE_BAR_WIDTH = 4; - this.refreshVisualization(); + this.refreshRepresentation(); } SongEditor.prototype.setData = function(songData) { this.songData = songData; -} - - -function isPointInside(p, rect) -{ - return ( - p.x >= rect.x1 && p.x <= rect.x2 && - p.y >= rect.y1 && p.y <= rect.y2); -} - - -function transformMousePosition(elem, ev) -{ - var rect = elem.getBoundingClientRect(); - return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; -} - - -SongEditor.prototype.handleMouseMove = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - this.canvas.style.cursor = "default"; - - this.hoverNote = -1; - for (var b = 0; b < this.viewBlocks.length; b++) - { - if (isPointInside(mousePos, this.viewBlocks[b])) - { - for (var n = 0; n < this.viewBlocks[b].notes.length; n++) - { - var note = this.viewBlocks[b].notes[n]; - if (isPointInside(mousePos, note)) - { - this.hoverNote = note.noteIndex; - - if (mousePos.x <= note.trueX1 + 2 || mousePos.x >= note.trueX2 - 2) - this.canvas.style.cursor = "ew-resize"; - else - this.canvas.style.cursor = "pointer"; - - break; - } - } - break; - } - } - - this.refreshCanvas(); -} - - -SongEditor.prototype.refreshVisualization = function() -{ - this.viewBlocks = []; - this.viewNotes = []; - this.viewChords = []; - this.viewKeyChanges = []; - this.viewMeterChanges = []; - - this.noteSelections = []; - for (var i = 0; i < this.songData.notes.length; i++) - this.noteSelections.push(false); - - var blockY1 = this.MARGIN_TOP + this.HEADER_MARGIN; - var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; - var changeY1 = this.MARGIN_TOP; - var chordY2 = this.canvasHeight - this.MARGIN_BOTTOM; - - var x = this.MARGIN_LEFT; - var tick = 0; - var curNote = 0; - var curChord = 0; - var curKeyChange = 0; - var curMeterChange = 0; - - var curBlock = 0; - this.viewBlocks.push( - { - tick: 0, - duration: 0, - key: null, - meter: null, - notes: [], - x1: x, - y1: blockY1, - x2: x, - y2: blockY2 - }); - - var NEXT_IS_NONE = 0; - var NEXT_IS_KEYCHANGE = 1; - var NEXT_IS_METERCHANGE = 2; - - while (true) - { - // Find the tick where the current block ends. - var nextChangeTick = this.songData.lastTick; - var nextIsWhat = NEXT_IS_NONE; - - if (curKeyChange < this.songData.keyChanges.length) - { - var nextKeyChange = this.songData.keyChanges[curKeyChange] - if (nextKeyChange.tick < nextChangeTick) - { - nextChangeTick = nextKeyChange.tick; - nextIsWhat = NEXT_IS_KEYCHANGE; - } - } - - if (curMeterChange < this.songData.meterChanges.length) - { - var nextMeterChange = this.songData.meterChanges[curMeterChange] - if (nextMeterChange.tick < nextChangeTick) - { - nextChangeTick = nextMeterChange.tick; - nextIsWhat = NEXT_IS_METERCHANGE; - } - } - - // Advance draw position until the next tick. - x += (nextChangeTick - tick) * this.tickZoom; - tick = nextChangeTick; - - // If there is a key change, add its visualization and advance its iterator. - var blockX2 = x; - - if (nextIsWhat == NEXT_IS_KEYCHANGE) - { - x += this.KEYCHANGE_BAR_WIDTH; - - this.viewKeyChanges.push( - { - keyChange: this.songData.keyChanges[curKeyChange], - tick: nextChangeTick, - x1: blockX2, - y1: changeY1, - x2: x, - y2: chordY2 - }); - - curKeyChange++; - } - // Or if there is a meter change, add its visualization and advance its iterator. - else if (nextIsWhat == NEXT_IS_METERCHANGE) - { - x += this.METERCHANGE_BAR_WIDTH; - - this.viewMeterChanges.push( - { - meterChange: this.songData.meterChanges[curMeterChange], - tick: nextChangeTick, - x1: blockX2, - y1: changeY1, - x2: x, - y2: chordY2 - }); - - curMeterChange++; - } - - // Then finish off the current block of notes. - // If its duration would be zero, ignore it for now but prepare it for the next iteration. - var block = this.viewBlocks[curBlock]; - - if (nextChangeTick == block.tick) - { - block.x1 = x; - block.x2 = x; - } - else - { - block.duration = nextChangeTick - block.tick; - block.x2 = blockX2; - - for (var n = 0; n < this.songData.notes.length; n++) - { - var note = this.songData.notes[n]; - var noteRow = this.getNoteRow(note.pitch, block.key.scale); - var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); - block.notes.push( - { - noteIndex: n, - tick: note.tick, - duration: note.duration, - trueX1: notePos.trueX1, - trueX2: notePos.trueX2, - x1: notePos.x1, - y1: notePos.y1, - x2: notePos.x2, - y2: notePos.y2 - }); - } - - // If this is the final block, we can stop now. - if (nextIsWhat == NEXT_IS_NONE) - break; - - // Or else, add a new block for the next iteration. - this.viewBlocks.push( - { - tick: tick, - duration: 0, - key: block.key, - meter: block.meter, - notes: [], - x1: x, - y1: blockY1, - x2: x, - y2: blockY2 - }); - - curBlock++; - } - - // Apply key/meter changes to the following block. - if (nextIsWhat == NEXT_IS_KEYCHANGE) - this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; - else if (nextIsWhat == NEXT_IS_METERCHANGE) - this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; - } -} - - -// Returns the row number where a note of the given pitch would be placed, -// according to the given scale. -SongEditor.prototype.getNoteRow = function(pitch, scale) -{ - var pitchInOctave = (pitch % 12); - var pitchDegree = 0; - - for (var i = 0; i < scale.degrees.length; i++) - { - if (scale.degrees[i] == pitchInOctave) - { - pitchDegree = i; - break; - } - else if (scale.degrees[i] > pitchInOctave) - { - pitchDegree = i - 0.5; - break; - } - } - - return pitchDegree; -} - - -// Returns a color string matching the given row. -SongEditor.prototype.getColorForRow = function(row) -{ - var colors = - [ - "#ff0000", - "#ff8800", - "#eeee00", - "#44dd33", - "#0000ff", - "#8800ff", - "#ff00ff" - ]; - return colors[row - (row % 1)]; -} - - -SongEditor.prototype.getNotePosition = function(block, row, tick, duration) -{ - var blockTick = tick - block.tick; - return { - trueX1: block.x1 + blockTick * this.tickZoom, - trueX2: block.x1 + (blockTick + duration) * this.tickZoom, - x1: block.x1 + Math.max(0, blockTick * this.tickZoom), - x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom), - y1: block.y2 - (row + 1) * this.NOTE_HEIGHT, - y2: block.y2 - (row) * this.NOTE_HEIGHT, - }; -} - - -SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hovering, selected) -{ - var block = this.viewBlocks[blockIndex]; - - if (tick + duration <= block.tick || - tick >= block.tick + block.duration) - return; - - var row = this.getNoteRow(pitch, block.key.scale); - var pos = this.getNotePosition(block, row, tick, duration); - var col = this.getColorForRow(row); - - this.ctx.save(); - this.ctx.fillStyle = col; - if (hovering) - { - this.ctx.globalAlpha = 0.5; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, 3); - this.ctx.fillRect(pos.x1, pos.y2 - 3, pos.x2 - pos.x1, 3); - } - else - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - - this.ctx.globalAlpha = 0.5; - - if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) - { - var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = this.getNoteRow(pitch, nextBlock.key.scale); - - var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; - var nextY2 = nextY1 + this.NOTE_HEIGHT; - - this.ctx.beginPath(); - this.ctx.moveTo(block.x2, pos.y1); - this.ctx.lineTo(nextBlock.x1, nextY1); - this.ctx.lineTo(nextBlock.x1, nextY2); - this.ctx.lineTo(block.x2, pos.y2); - this.ctx.fill(); - } - - this.ctx.restore(); -} - - -SongEditor.prototype.refreshCanvas = function() -{ - this.ctx.fillStyle = "white"; - this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); - - for (var i = 0; i < this.viewBlocks.length; i++) - { - var block = this.viewBlocks[i]; - - for (var n = 0; n < block.notes.length; n++) - { - var note = this.songData.notes[block.notes[n].noteIndex]; - this.drawNote(i, note.pitch, note.tick, note.duration, block.notes[n].noteIndex == this.hoverNote, false); - } - - this.ctx.strokeStyle = "black"; - this.ctx.lineWidth = 2; - - var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); - this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - block.y1); - this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); - } - - for (var i = 0; i < this.viewKeyChanges.length; i++) - { - var keyChange = this.viewKeyChanges[i]; - - this.ctx.fillStyle = "red"; - this.ctx.fillRect((keyChange.x1 + keyChange.x2) / 2 - 1, keyChange.y1, 2, keyChange.y2 - keyChange.y1); - } - - for (var i = 0; i < this.viewMeterChanges.length; i++) - { - var meterChange = this.viewMeterChanges[i]; - - this.ctx.fillStyle = "blue"; - this.ctx.fillRect((meterChange.x1 + meterChange.x2) / 2 - 1, meterChange.y1, 2, meterChange.y2 - meterChange.y1); - } } \ No newline at end of file diff --git a/src/editor_drawing.js b/src/editor_drawing.js new file mode 100644 index 0000000..deec54b --- /dev/null +++ b/src/editor_drawing.js @@ -0,0 +1,197 @@ +SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hovering, selected) +{ + var block = this.viewBlocks[blockIndex]; + + if (tick + duration <= block.tick || + tick >= block.tick + block.duration) + return; + + var row = this.getNoteRow(pitch, block.key.scale); + var pos = this.getNotePosition(block, row, tick, duration); + var col = this.getColorForRow(row); + + this.ctx.save(); + this.ctx.fillStyle = col; + if (selected) + { + this.ctx.globalAlpha = 0.5; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, 3); + this.ctx.fillRect(pos.x1, pos.y2 - 3, pos.x2 - pos.x1, 3); + } + else if (hovering) + { + this.ctx.globalAlpha = 0.5; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + } + else + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + + this.ctx.globalAlpha = 0.5; + + if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) + { + var nextBlock = this.viewBlocks[blockIndex + 1]; + var nextRow = this.getNoteRow(pitch, nextBlock.key.scale); + + var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; + var nextY2 = nextY1 + this.NOTE_HEIGHT; + + this.ctx.beginPath(); + this.ctx.moveTo(block.x2, pos.y1); + this.ctx.lineTo(nextBlock.x1, nextY1); + this.ctx.lineTo(nextBlock.x1, nextY2); + this.ctx.lineTo(block.x2, pos.y2); + this.ctx.fill(); + } + + this.ctx.restore(); +} + + +SongEditor.prototype.refreshCanvas = function() +{ + this.ctx.fillStyle = "white"; + this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); + + // Draw notes. + for (var i = 0; i < this.viewBlocks.length; i++) + { + var block = this.viewBlocks[i]; + + for (var n = 0; n < block.notes.length; n++) + { + var noteIndex = block.notes[n].noteIndex; + var note = this.songData.notes[noteIndex]; + + if (this.noteSelections[noteIndex] && this.mouseDragAction != null) + { + var draggedNote = this.getNoteDragged(note, this.mouseDragCurrent); + this.drawNote(i, draggedNote.pitch, draggedNote.tick, draggedNote.duration, noteIndex == this.hoverNote, true); + } + else + this.drawNote(i, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); + } + + this.ctx.strokeStyle = "black"; + this.ctx.lineWidth = 2; + + var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); + this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - block.y1); + this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); + } + + this.ctx.font = "14px Tahoma"; + this.ctx.textAlign = "left"; + this.ctx.textBaseline = "top"; + + // Draw key changes. + for (var i = 0; i < this.viewKeyChanges.length; i++) + { + var keyChange = this.viewKeyChanges[i]; + var textX = keyChange.x2 + 8; + + if (this.keyChangeSelections[i]) + { + if (this.mouseDragAction != null) + { + var draggedKeyChange = this.getKeyChangeDragged(keyChange, this.mouseDragCurrent); + + if (draggedKeyChange.tick == keyChange.tick) + { + this.ctx.fillStyle = "#eeeeee"; + this.ctx.fillRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); + this.ctx.strokeStyle = "#aaaaaa"; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); + } + else + { + var x = this.getPositionForTick(draggedKeyChange.tick); + this.ctx.strokeStyle = "#aaaaaa"; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(x, keyChange.y1); + this.ctx.lineTo(x, keyChange.y2); + this.ctx.stroke(); + } + } + else + { + this.ctx.fillStyle = "#eeeeee"; + this.ctx.fillRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); + this.ctx.strokeStyle = "#aaaaaa"; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); + } + } + else + { + this.ctx.strokeStyle = (this.hoverKeyChange == i ? "#cccccc" : "#aaaaaa"); + this.ctx.lineWidth = 2; + this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); + } + + var songKeyChange = this.songData.keyChanges[keyChange.keyChangeIndex]; + this.ctx.fillStyle = "#aaaaaa"; + this.ctx.fillText( + "" + theoryNoteName(songKeyChange.tonicPitch) + " " + songKeyChange.scale.name, + textX, + keyChange.y1); + } + + // Draw meter changes. + for (var i = 0; i < this.viewMeterChanges.length; i++) + { + var meterChange = this.viewMeterChanges[i]; + var textX = meterChange.x2 + 8; + + if (this.meterChangeSelections[i]) + { + if (this.mouseDragAction != null) + { + var draggedMeterChange = this.getMeterChangeDragged(meterChange, this.mouseDragCurrent); + + if (draggedMeterChange.tick == meterChange.tick) + { + this.ctx.fillStyle = "#aaeeee"; + this.ctx.fillRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); + this.ctx.strokeStyle = "#88aaaa"; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); + } + else + { + var x = this.getPositionForTick(draggedMeterChange.tick); + this.ctx.strokeStyle = "#88aaaa"; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(x, meterChange.y1); + this.ctx.lineTo(x, meterChange.y2); + this.ctx.stroke(); + } + } + else + { + this.ctx.fillStyle = "#aaeeee"; + this.ctx.fillRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); + this.ctx.strokeStyle = "#88aaaa"; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); + } + } + else + { + this.ctx.strokeStyle = (this.hovermeterChange == i ? "#99cccc" : "#88aaaa"); + this.ctx.lineWidth = 2; + this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); + } + + var songMeterChange = this.songData.meterChanges[meterChange.meterChangeIndex]; + this.ctx.fillStyle = "#88aaaa"; + this.ctx.fillText( + "" + songMeterChange.numerator + " / " + songMeterChange.denominator, + textX, + meterChange.y1); + } +} \ No newline at end of file diff --git a/src/editor_helpers.js b/src/editor_helpers.js new file mode 100644 index 0000000..b32dde9 --- /dev/null +++ b/src/editor_helpers.js @@ -0,0 +1,55 @@ +// Returns the row number where a note of the given pitch would be placed, +// according to the given scale. +SongEditor.prototype.getNoteRow = function(pitch, scale) +{ + var pitchInOctave = (pitch % 12); + var pitchDegree = scale.degrees.length - 0.5; + + for (var i = 0; i < scale.degrees.length; i++) + { + if (scale.degrees[i] == pitchInOctave) + { + pitchDegree = i; + break; + } + else if (scale.degrees[i] > pitchInOctave) + { + pitchDegree = i - 0.5; + break; + } + } + + return pitchDegree + (Math.floor(pitch / 12) * scale.degrees.length); +} + + +// Returns the bounds of the given row's note's representation rectangle. +SongEditor.prototype.getNotePosition = function(block, row, tick, duration) +{ + var blockTick = tick - block.tick; + return { + resizeHandleL: block.x1 + blockTick * this.tickZoom, + resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, + x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), + x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), + y1: block.y2 - (row + 1) * this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, + y2: block.y2 - (row) * this.NOTE_HEIGHT - this.NOTE_MARGIN_VER, + }; +} + + +// Returns a color string matching the given row. +SongEditor.prototype.getColorForRow = function(row) +{ + var colors = + [ + "#fb3214", + "#f7a610", + "#f2f201", + "#82f21e", + "#01beca", + "#6b16fc", + "#ed18a8" + ]; + return colors[(row - (row % 1)) % 7]; +} diff --git a/src/editor_interaction.js b/src/editor_interaction.js new file mode 100644 index 0000000..255f0e3 --- /dev/null +++ b/src/editor_interaction.js @@ -0,0 +1,398 @@ +SongEditor.prototype.unselectAll = function() +{ + for (var i = 0; i < this.noteSelections.length; i++) + this.noteSelections[i] = false; + + for (var i = 0; i < this.keyChangeSelections.length; i++) + this.keyChangeSelections[i] = false; + + for (var i = 0; i < this.meterChangeSelections.length; i++) + this.meterChangeSelections[i] = false; +} + + +SongEditor.prototype.clearHover = function() +{ + this.hoverNote = -1; + this.hoverKeyChange = -1; + this.hoverMeterChange = -1; + this.hoverStretchR = false; + this.hoverStretchL = false; +} + + +function isPointInside(p, rect) +{ + return ( + p.x >= rect.x1 && p.x <= rect.x2 && + p.y >= rect.y1 && p.y <= rect.y2); +} + + +function transformMousePosition(elem, ev) +{ + var rect = elem.getBoundingClientRect(); + return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; +} + + +SongEditor.prototype.getPositionForTick = function(tick) +{ + for (var b = 0; b < this.viewBlocks.length; b++) + { + var block = this.viewBlocks[b]; + + if (tick >= block.tick && tick < block.tick + block.duration) + { + return block.x1 + (tick - block.tick) * this.tickZoom; + } + } + + return 0; +} + + +SongEditor.prototype.getTickAtPosition = function(x) +{ + for (var b = 0; b < this.viewBlocks.length; b++) + { + var block = this.viewBlocks[b]; + + if (x >= block.x1 && + (b == this.viewBlocks.length - 1 || x <= this.viewBlocks[b + 1].x1)) + { + return Math.round((block.tick + Math.min(block.duration, Math.round((x - block.x1) / this.tickZoom))) / this.tickSnap) * this.tickSnap; + } + } + + return 0; +} + + +SongEditor.prototype.getEarliestSelectedTick = function() +{ + // FIXME: Take chords and key/meter changes into consideration. + // TODO: Use binary search. + for (var n = 0; n < this.noteSelections.length; n++) + { + if (this.noteSelections[n]) + return this.songData.notes[n].tick; + } + + return 0; +} + + +SongEditor.prototype.getLatestSelectedTick = function() +{ + // FIXME: Take chords and key/meter changes into consideration. + // TODO: Use binary search. + for (var n = this.noteSelections.length - 1; n >= 0; n--) + { + if (this.noteSelections[n]) + return this.songData.notes[n].tick + this.songData.notes[n].duration; + } + + return 0; +} + + +SongEditor.prototype.getNoteDragged = function(note, dragPosition) +{ + var dragTick = this.getTickAtPosition(dragPosition.x); + var pitchOffset = Math.round((this.mouseDragOrigin.y - dragPosition.y) / this.NOTE_HEIGHT * 2); + var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin.x); + + if (this.mouseDragAction == "move") + { + return { + tick: note.tick + tickOffset, + duration: note.duration, + pitch: note.pitch + pitchOffset + }; + } + else if (this.mouseDragAction == "stretch") + { + var x1Proportion = (note.tick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + var x2Proportion = (note.tick + note.duration - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + var mouseProportion = (dragTick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + + var pivotDist = (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + var newX1 = Math.round((this.mouseStretchPivotTick + pivotDist * (x1Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; + var newX2 = Math.round((this.mouseStretchPivotTick + pivotDist * (x2Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; + + if (newX2 < newX1) + { + var temp = newX1; + newX1 = newX2; + newX2 = temp; + } + + return { + tick: newX1, + duration: newX2 - newX1, + pitch: note.pitch + pitchOffset + }; + } +} + + +SongEditor.prototype.getKeyChangeDragged = function(keyChange, dragPosition) +{ + var tickOffset = this.getTickAtPosition(dragPosition.x) - this.getTickAtPosition(this.mouseDragOrigin.x); + + if (this.mouseDragAction == "move") + return { tick: keyChange.tick + tickOffset }; + + // TODO: Should move proportionally, like notes. + else if (this.mouseDragAction == "stretch") + return { tick: keyChange.tick }; +} + + +SongEditor.prototype.getMeterChangeDragged = function(meterChange, dragPosition) +{ + var tickOffset = this.getTickAtPosition(dragPosition.x) - this.getTickAtPosition(this.mouseDragOrigin.x); + + if (this.mouseDragAction == "move") + return { tick: meterChange.tick + tickOffset }; + + // TODO: Should move proportionally, like notes. + else if (this.mouseDragAction == "stretch") + return { tick: meterChange.tick }; +} + + +SongEditor.prototype.handleMouseMove = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + this.canvas.style.cursor = "default"; + this.mouseDragCurrent = mousePos; + this.clearHover(); + + // Check what's under the mouse, if it's not down. + if (!this.mouseDown) + { + // Check for notes. + for (var b = 0; b < this.viewBlocks.length; b++) + { + if (isPointInside(mousePos, this.viewBlocks[b])) + { + for (var n = 0; n < this.viewBlocks[b].notes.length; n++) + { + var note = this.viewBlocks[b].notes[n]; + if (isPointInside(mousePos, note)) + { + this.hoverNote = note.noteIndex; + + if (mousePos.x <= note.resizeHandleL + 2) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchL = true; + } + else if (mousePos.x >= note.resizeHandleR - 2) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchR = true; + } + else + this.canvas.style.cursor = "pointer"; + + break; + } + } + break; + } + } + + // Check for key changes. + if (this.hoverNote < 0) + { + for (var n = 0; n < this.viewKeyChanges.length; n++) + { + var keyChange = this.viewKeyChanges[n]; + if (isPointInside(mousePos, keyChange)) + { + this.hoverKeyChange = keyChange.keyChangeIndex; + this.canvas.style.cursor = "ew-resize"; + break; + } + } + } + + // Check for meter changes. + if (this.hoverNote < 0 && this.hoverKeyChange < 0) + { + for (var n = 0; n < this.viewMeterChanges.length; n++) + { + var meterChange = this.viewMeterChanges[n]; + if (isPointInside(mousePos, meterChange)) + { + this.hoverMeterChange = meterChange.meterChangeIndex; + this.canvas.style.cursor = "ew-resize"; + break; + } + } + } + } + else if (this.mouseDragAction == "move") + this.canvas.style.cursor = "move"; + else if (this.mouseDragAction == "stretch") + this.canvas.style.cursor = "ew-resize"; + + this.refreshCanvas(); +} + + +SongEditor.prototype.handleMouseDown = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + if (!ev.ctrlKey) + this.unselectAll(); + + this.mouseDragAction = null; + this.mouseDragOrigin = mousePos; + + // Start a dragging operation. + if (this.hoverNote >= 0) + { + this.noteSelections[this.hoverNote] = true; + if (this.hoverStretchR) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick + this.songData.notes[this.hoverNote].duration; + this.mouseStretchPivotTick = this.getEarliestSelectedTick(); + } + else if (this.hoverStretchL) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick; + this.mouseStretchPivotTick = this.getLatestSelectedTick(); + } + else + this.mouseDragAction = "move"; + } + else if (this.hoverKeyChange >= 0) + { + this.keyChangeSelections[this.hoverKeyChange] = true; + this.mouseDragAction = "move"; + } + else if (this.hoverMeterChange >= 0) + { + this.meterChangeSelections[this.hoverMeterChange] = true; + this.mouseDragAction = "move"; + } + + this.mouseDown = true; + this.refreshCanvas(); +} + + +SongEditor.prototype.handleMouseUp = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + // Apply dragged modifications. + if (this.mouseDown && this.mouseDragAction != null) + { + // Store modified objects in a local array and + // remove them from the song data. + var selectedNotes = []; + for (var n = this.noteSelections.length - 1; n >= 0; n--) + { + if (this.noteSelections[n]) + { + selectedNotes.push(this.songData.notes[n]); + this.songData.notes.splice(n, 1); + } + } + + var selectedKeyChanges = []; + for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) + { + if (this.keyChangeSelections[n]) + { + selectedKeyChanges.push(this.songData.keyChanges[n]); + this.songData.keyChanges.splice(n, 1); + } + } + + var selectedMeterChanges = []; + for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) + { + if (this.meterChangeSelections[n]) + { + selectedMeterChanges.push(this.songData.meterChanges[n]); + this.songData.meterChanges.splice(n, 1); + } + } + + // Apply the modification and reinsert objects into the song data. + var newNotes = []; + for (var n = 0; n < selectedNotes.length; n++) + { + var draggedNote = this.getNoteDragged(selectedNotes[n], mousePos); + var newNote = new SongDataNote(draggedNote.tick, draggedNote.duration, draggedNote.pitch); + newNotes.push(newNote); + this.songData.addNote(newNote); + } + + var newKeyChanges = []; + for (var n = 0; n < selectedKeyChanges.length; n++) + { + var draggedKeyChange = this.getKeyChangeDragged(selectedKeyChanges[n], mousePos); + var newKeyChange = new SongDataKeyChange(draggedKeyChange.tick, selectedKeyChanges[n].scale, selectedKeyChanges[n].tonicPitch); + newKeyChanges.push(newKeyChange); + this.songData.addKeyChange(newKeyChange); + } + + var newMeterChanges = []; + for (var n = 0; n < selectedMeterChanges.length; n++) + { + var draggedMeterChange = this.getMeterChangeDragged(selectedMeterChanges[n], mousePos); + var newMeterChange = new SongDataMeterChange(draggedMeterChange.tick, selectedMeterChanges[n].numerator, selectedMeterChanges[n].denominator); + newMeterChanges.push(newMeterChange); + this.songData.addMeterChange(newMeterChange); + } + + this.clearHover(); + this.refreshRepresentation(); + + // Find which objects were selected before, and select them again. + for (var n = 0; n < this.songData.notes.length; n++) + { + for (var m = 0; m < newNotes.length; m++) + { + if (this.songData.notes[n] == newNotes[m]) + this.noteSelections[n] = true; + } + } + + for (var n = 0; n < this.songData.keyChanges.length; n++) + { + for (var m = 0; m < newKeyChanges.length; m++) + { + if (this.songData.keyChanges[n] == newKeyChanges[m]) + this.keyChangeSelections[n] = true; + } + } + + for (var n = 0; n < this.songData.meterChanges.length; n++) + { + for (var m = 0; m < newMeterChanges.length; m++) + { + if (this.songData.meterChanges[n] == newMeterChanges[m]) + this.meterChangeSelections[n] = true; + } + } + } + + this.mouseDown = false; + this.mouseDragAction = null; + this.refreshCanvas(); +} \ No newline at end of file diff --git a/src/editor_representation.js b/src/editor_representation.js new file mode 100644 index 0000000..38e6a20 --- /dev/null +++ b/src/editor_representation.js @@ -0,0 +1,180 @@ +// Sets up representation objects for the data in the song, +// which are used to draw the staff and to interact with the mouse. +SongEditor.prototype.refreshRepresentation = function() +{ + this.viewBlocks = []; + this.viewNotes = []; + this.viewChords = []; + this.viewKeyChanges = []; + this.viewMeterChanges = []; + + this.noteSelections = []; + for (var i = 0; i < this.songData.notes.length; i++) + this.noteSelections.push(false); + + this.keyChangeSelections = []; + for (var i = 0; i < this.songData.keyChanges.length; i++) + this.keyChangeSelections.push(false); + + this.meterChangeSelections = []; + for (var i = 0; i < this.songData.meterChanges.length; i++) + this.meterChangeSelections.push(false); + + var blockY1 = this.MARGIN_TOP + this.HEADER_MARGIN; + var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + var changeY1 = this.MARGIN_TOP; + var chordY2 = this.canvasHeight - this.MARGIN_BOTTOM; + + var x = this.MARGIN_LEFT; + var tick = 0; + var curNote = 0; + var curChord = 0; + var curKeyChange = 0; + var curMeterChange = 0; + + var curBlock = 0; + this.viewBlocks.push( + { + tick: 0, + duration: 0, + key: new SongDataKeyChange(0, scaleMajor, 0), + meter: null, + notes: [], + x1: x, + y1: blockY1, + x2: x, + y2: blockY2 + }); + + var NEXT_IS_NONE = 0; + var NEXT_IS_KEYCHANGE = 1; + var NEXT_IS_METERCHANGE = 2; + + while (true) + { + // Find the tick where the current block ends. + var nextChangeTick = this.songData.lastTick; + var nextIsWhat = NEXT_IS_NONE; + + if (curKeyChange < this.songData.keyChanges.length) + { + var nextKeyChange = this.songData.keyChanges[curKeyChange] + if (nextKeyChange.tick < nextChangeTick) + { + nextChangeTick = nextKeyChange.tick; + nextIsWhat = NEXT_IS_KEYCHANGE; + } + } + + if (curMeterChange < this.songData.meterChanges.length) + { + var nextMeterChange = this.songData.meterChanges[curMeterChange] + if (nextMeterChange.tick < nextChangeTick) + { + nextChangeTick = nextMeterChange.tick; + nextIsWhat = NEXT_IS_METERCHANGE; + } + } + + // Advance draw position until the next tick. + x += (nextChangeTick - tick) * this.tickZoom; + tick = nextChangeTick; + + // If there is a key change, add its visualization and advance its iterator. + var blockX2 = x; + + if (nextIsWhat == NEXT_IS_KEYCHANGE) + { + x += this.KEYCHANGE_BAR_WIDTH; + + this.viewKeyChanges.push( + { + keyChangeIndex: curKeyChange, + tick: nextChangeTick, + x1: blockX2, + y1: changeY1, + x2: x, + y2: chordY2 + }); + + curKeyChange++; + } + // Or if there is a meter change, add its visualization and advance its iterator. + else if (nextIsWhat == NEXT_IS_METERCHANGE) + { + x += this.METERCHANGE_BAR_WIDTH; + + this.viewMeterChanges.push( + { + meterChangeIndex: curMeterChange, + tick: nextChangeTick, + x1: blockX2, + y1: changeY1 + 20, + x2: x, + y2: chordY2 + }); + + curMeterChange++; + } + + // Then finish off the current block of notes. + // If its duration would be zero, ignore it for now but prepare it for the next iteration. + var block = this.viewBlocks[curBlock]; + + if (nextChangeTick == block.tick) + { + block.x1 = x; + block.x2 = x; + } + else + { + block.duration = nextChangeTick - block.tick; + block.x2 = blockX2; + + for (var n = 0; n < this.songData.notes.length; n++) + { + var note = this.songData.notes[n]; + var noteRow = this.getNoteRow(note.pitch, block.key.scale); + var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); + block.notes.push( + { + noteIndex: n, + tick: note.tick, + duration: note.duration, + resizeHandleL: notePos.resizeHandleL, + resizeHandleR: notePos.resizeHandleR, + x1: notePos.x1, + y1: notePos.y1, + x2: notePos.x2, + y2: notePos.y2 + }); + } + + // If this is the final block, we can stop now. + if (nextIsWhat == NEXT_IS_NONE) + break; + + // Or else, add a new block for the next iteration. + this.viewBlocks.push( + { + tick: tick, + duration: 0, + key: block.key, + meter: block.meter, + notes: [], + x1: x, + y1: blockY1, + x2: x, + y2: blockY2 + }); + + curBlock++; + } + + // Apply key/meter changes to the following block. + if (nextIsWhat == NEXT_IS_KEYCHANGE) + this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; + else if (nextIsWhat == NEXT_IS_METERCHANGE) + this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; + } +} \ No newline at end of file diff --git a/src/theory.js b/src/theory.js new file mode 100644 index 0000000..6488cb3 --- /dev/null +++ b/src/theory.js @@ -0,0 +1,5 @@ +function theoryNoteName(pitch, scale) +{ + var notes = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; + return notes[pitch % 12]; +} \ No newline at end of file diff --git a/test.html b/test.html index b52d5f8..ad619e0 100644 --- a/test.html +++ b/test.html @@ -12,6 +12,11 @@ + + + + + \ No newline at end of file diff --git a/test.js b/test.js index 4d4c1e6..7c1729c 100644 --- a/test.js +++ b/test.js @@ -2,13 +2,27 @@ function testOnLoad() { var song = new SongData(); song.addKeyChange(new SongDataKeyChange(0, scaleMajor, 0)); + song.addMeterChange(new SongDataMeterChange(0, 4, 4)); song.addKeyChange(new SongDataKeyChange(100, scaleDorian, 0)); - song.addKeyChange(new SongDataKeyChange(200, scaleMajor, 0)); - song.addKeyChange(new SongDataKeyChange(300, scaleMajor, 0)); + song.addMeterChange(new SongDataMeterChange(150, 3, 4)); + song.addKeyChange(new SongDataKeyChange(200, scaleMixolydian, 0)); + song.addKeyChange(new SongDataKeyChange(300, scalePhrygianDominant, 0)); song.addNote(new SongDataNote(0, 300, 0)); song.addNote(new SongDataNote(0, 300, 3)); - song.addNote(new SongDataNote(0, 300, 8)); + song.addNote(new SongDataNote(0, 300, 5)); + song.addNote(new SongDataNote(325, 25, 0)); + song.addNote(new SongDataNote(350, 25, 1)); + song.addNote(new SongDataNote(375, 25, 2)); + song.addNote(new SongDataNote(400, 25, 3)); + song.addNote(new SongDataNote(425, 25, 4)); + song.addNote(new SongDataNote(450, 25, 5)); + song.addNote(new SongDataNote(475, 25, 6)); + song.addNote(new SongDataNote(500, 25, 7)); + song.addNote(new SongDataNote(525, 25, 8)); + song.addNote(new SongDataNote(550, 25, 9)); + song.addNote(new SongDataNote(575, 25, 10)); + song.addNote(new SongDataNote(600, 25, 11)); var canvas = document.getElementById("editorCanvas"); var editor = new SongEditor(canvas, song); From 945574d73196cffb4349c8605125999d90e89442 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 26 Nov 2015 23:56:48 -0200 Subject: [PATCH 05/46] add degree labels in key changes; make note positioning aware of key tonic --- src/editor.js | 3 +- src/editor_drawing.js | 54 +++++++++++++++++++++++++++++++++--- src/editor_helpers.js | 43 ++++++++++++++++++++++------ src/editor_interaction.js | 6 ++-- src/editor_representation.js | 4 +-- test.html | 1 + test.js | 6 ++-- 7 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/editor.js b/src/editor.js index cc43add..f1f4d91 100644 --- a/src/editor.js +++ b/src/editor.js @@ -36,6 +36,7 @@ function SongEditor(canvas, songData) this.hoverStretchR = false; this.hoverStretchL = false; + this.WHOLE_NOTE_DURATION = 100; this.MARGIN_LEFT = 4; this.MARGIN_RIGHT = 4; this.MARGIN_TOP = 4; @@ -46,7 +47,7 @@ function SongEditor(canvas, songData) this.NOTE_MARGIN_VER = 0.5; this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; - this.KEYCHANGE_BAR_WIDTH = 10; + this.KEYCHANGE_BAR_WIDTH = 20; this.METERCHANGE_BAR_WIDTH = 4; this.refreshRepresentation(); diff --git a/src/editor_drawing.js b/src/editor_drawing.js index deec54b..f36f501 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -6,7 +6,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove tick >= block.tick + block.duration) return; - var row = this.getNoteRow(pitch, block.key.scale); + var row = this.getNoteRow(pitch, block.key); var pos = this.getNotePosition(block, row, tick, duration); var col = this.getColorForRow(row); @@ -32,7 +32,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) { var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = this.getNoteRow(pitch, nextBlock.key.scale); + var nextRow = this.getNoteRow(pitch, nextBlock.key); var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; var nextY2 = nextY1 + this.NOTE_HEIGHT; @@ -54,11 +54,40 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.fillStyle = "white"; this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); - // Draw notes. + // Draw blocks. for (var i = 0; i < this.viewBlocks.length; i++) { var block = this.viewBlocks[i]; + // Draw rows. + for (var row = 0; row < 14; row++) + { + this.ctx.strokeStyle = (this.getNoteForRow(row, block.key) == block.key.tonicPitch ? "#bbbbbb" : "#dddddd"); + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(block.x1, block.y2 - row * this.NOTE_HEIGHT); + this.ctx.lineTo(block.x2, block.y2 - row * this.NOTE_HEIGHT); + this.ctx.stroke(); + } + + // Draw measures. + var submeasureCount = 0; + for (var n = block.meter.tick - block.tick; n < block.duration; n += this.WHOLE_NOTE_DURATION / block.meter.denominator) + { + if (n >= 0) + { + this.ctx.strokeStyle = (submeasureCount == 0 ? "#bbbbbb" : "#dddddd"); + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y1); + this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); + this.ctx.stroke(); + } + + submeasureCount = (submeasureCount + 1) % block.meter.numerator; + } + + // Draw notes. for (var n = 0; n < block.notes.length; n++) { var noteIndex = block.notes[n].noteIndex; @@ -73,6 +102,7 @@ SongEditor.prototype.refreshCanvas = function() this.drawNote(i, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); } + // Draw borders. this.ctx.strokeStyle = "black"; this.ctx.lineWidth = 2; @@ -81,11 +111,12 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); } + + // Draw key changes. this.ctx.font = "14px Tahoma"; this.ctx.textAlign = "left"; this.ctx.textBaseline = "top"; - // Draw key changes. for (var i = 0; i < this.viewKeyChanges.length; i++) { var keyChange = this.viewKeyChanges[i]; @@ -132,15 +163,30 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); } + this.ctx.font = "14px Tahoma"; var songKeyChange = this.songData.keyChanges[keyChange.keyChangeIndex]; this.ctx.fillStyle = "#aaaaaa"; this.ctx.fillText( "" + theoryNoteName(songKeyChange.tonicPitch) + " " + songKeyChange.scale.name, textX, keyChange.y1); + + this.ctx.font = "10px Tahoma"; + for (var row = 0; row < 14; row++) + { + this.ctx.fillStyle = "#444444"; + this.ctx.fillText( + "" + theoryNoteName(this.getNoteForRow(row, songKeyChange)), + keyChange.x1 + 4, + this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row + 1) * this.NOTE_HEIGHT); + } } + // Draw meter changes. + this.ctx.font = "14px Tahoma"; + this.ctx.textAlign = "left"; + for (var i = 0; i < this.viewMeterChanges.length; i++) { var meterChange = this.viewMeterChanges[i]; diff --git a/src/editor_helpers.js b/src/editor_helpers.js index b32dde9..7081a59 100644 --- a/src/editor_helpers.js +++ b/src/editor_helpers.js @@ -1,25 +1,50 @@ -// Returns the row number where a note of the given pitch would be placed, -// according to the given scale. -SongEditor.prototype.getNoteRow = function(pitch, scale) +// Returns the row index where a note of the given pitch would be placed, +// according to the given key. +SongEditor.prototype.getNoteRow = function(pitch, key) { - var pitchInOctave = (pitch % 12); - var pitchDegree = scale.degrees.length - 0.5; + var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); + var pitchDegree = key.scale.degrees.length - 0.5; - for (var i = 0; i < scale.degrees.length; i++) + var degreeOffsetFromC = 0; + if (key.tonicPitch != 0) { - if (scale.degrees[i] == pitchInOctave) + var originalTonicPitch = key.tonicPitch; + key.tonicPitch = 0; + degreeOffsetFromC = this.getNoteRow(originalTonicPitch, key); + key.tonicPitch = originalTonicPitch; + } + + for (var i = 0; i < key.scale.degrees.length; i++) + { + if (key.scale.degrees[i] == pitchInOctave) { pitchDegree = i; break; } - else if (scale.degrees[i] > pitchInOctave) + else if (key.scale.degrees[i] > pitchInOctave) { pitchDegree = i - 0.5; break; } } - return pitchDegree + (Math.floor(pitch / 12) * scale.degrees.length); + return pitchDegree + degreeOffsetFromC + (Math.floor((pitch - key.tonicPitch) / 12) * key.scale.degrees.length); +} + + +// Returns the scale degree of the given row index. +SongEditor.prototype.getNoteForRow = function(row, key) +{ + var degreeOffsetFromC = 0; + if (key.tonicPitch != 0) + { + var originalTonicPitch = key.tonicPitch; + key.tonicPitch = 0; + degreeOffsetFromC = this.getNoteRow(originalTonicPitch, key); + key.tonicPitch = originalTonicPitch; + } + + return key.scale.degrees[(row + key.scale.degrees.length - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch; } diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 255f0e3..5ea1c04 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -106,7 +106,7 @@ SongEditor.prototype.getNoteDragged = function(note, dragPosition) if (this.mouseDragAction == "move") { return { - tick: note.tick + tickOffset, + tick: Math.max(0, note.tick + tickOffset), duration: note.duration, pitch: note.pitch + pitchOffset }; @@ -129,9 +129,9 @@ SongEditor.prototype.getNoteDragged = function(note, dragPosition) } return { - tick: newX1, + tick: Math.max(0, newX1), duration: newX2 - newX1, - pitch: note.pitch + pitchOffset + pitch: note.pitch }; } } diff --git a/src/editor_representation.js b/src/editor_representation.js index 38e6a20..45ff808 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -38,7 +38,7 @@ SongEditor.prototype.refreshRepresentation = function() tick: 0, duration: 0, key: new SongDataKeyChange(0, scaleMajor, 0), - meter: null, + meter: new SongDataMeterChange(0, 4, 4), notes: [], x1: x, y1: blockY1, @@ -134,7 +134,7 @@ SongEditor.prototype.refreshRepresentation = function() for (var n = 0; n < this.songData.notes.length; n++) { var note = this.songData.notes[n]; - var noteRow = this.getNoteRow(note.pitch, block.key.scale); + var noteRow = this.getNoteRow(note.pitch, block.key); var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); block.notes.push( { diff --git a/test.html b/test.html index ad619e0..8fa55b2 100644 --- a/test.html +++ b/test.html @@ -4,6 +4,7 @@ Test + diff --git a/test.js b/test.js index 7c1729c..13de4b1 100644 --- a/test.js +++ b/test.js @@ -3,14 +3,14 @@ function testOnLoad() var song = new SongData(); song.addKeyChange(new SongDataKeyChange(0, scaleMajor, 0)); song.addMeterChange(new SongDataMeterChange(0, 4, 4)); - song.addKeyChange(new SongDataKeyChange(100, scaleDorian, 0)); + song.addKeyChange(new SongDataKeyChange(100, scaleMajor, 2)); song.addMeterChange(new SongDataMeterChange(150, 3, 4)); song.addKeyChange(new SongDataKeyChange(200, scaleMixolydian, 0)); song.addKeyChange(new SongDataKeyChange(300, scalePhrygianDominant, 0)); - song.addNote(new SongDataNote(0, 300, 0)); - song.addNote(new SongDataNote(0, 300, 3)); + song.addNote(new SongDataNote(0, 300, 12)); song.addNote(new SongDataNote(0, 300, 5)); + song.addNote(new SongDataNote(0, 300, 7)); song.addNote(new SongDataNote(325, 25, 0)); song.addNote(new SongDataNote(350, 25, 1)); song.addNote(new SongDataNote(375, 25, 2)); From 811e39d8ea457139776b03084ac680790e4b6ab8 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Fri, 27 Nov 2015 00:10:49 -0200 Subject: [PATCH 06/46] make note coloring aware of key tonic; fix bug with fractional scale degrees --- src/editor_drawing.js | 11 +++++----- src/editor_helpers.js | 40 ++++++++++++++++++++++++++++-------- src/editor_representation.js | 2 +- test.js | 4 ++-- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/editor_drawing.js b/src/editor_drawing.js index f36f501..9db0f4c 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -6,9 +6,10 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove tick >= block.tick + block.duration) return; - var row = this.getNoteRow(pitch, block.key); + var deg = this.getDegreeForPitch(pitch, block.key); + var row = this.getRowForPitch(pitch, block.key); var pos = this.getNotePosition(block, row, tick, duration); - var col = this.getColorForRow(row); + var col = this.getColorForDegree(deg); this.ctx.save(); this.ctx.fillStyle = col; @@ -32,7 +33,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) { var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = this.getNoteRow(pitch, nextBlock.key); + var nextRow = this.getRowForPitch(pitch, nextBlock.key); var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; var nextY2 = nextY1 + this.NOTE_HEIGHT; @@ -62,7 +63,7 @@ SongEditor.prototype.refreshCanvas = function() // Draw rows. for (var row = 0; row < 14; row++) { - this.ctx.strokeStyle = (this.getNoteForRow(row, block.key) == block.key.tonicPitch ? "#bbbbbb" : "#dddddd"); + this.ctx.strokeStyle = (this.getPitchForRow(row, block.key) == block.key.tonicPitch ? "#bbbbbb" : "#dddddd"); this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(block.x1, block.y2 - row * this.NOTE_HEIGHT); @@ -176,7 +177,7 @@ SongEditor.prototype.refreshCanvas = function() { this.ctx.fillStyle = "#444444"; this.ctx.fillText( - "" + theoryNoteName(this.getNoteForRow(row, songKeyChange)), + "" + theoryNoteName(this.getPitchForRow(row, songKeyChange)), keyChange.x1 + 4, this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row + 1) * this.NOTE_HEIGHT); } diff --git a/src/editor_helpers.js b/src/editor_helpers.js index 7081a59..58d9ada 100644 --- a/src/editor_helpers.js +++ b/src/editor_helpers.js @@ -1,6 +1,30 @@ +// Returns the scale degree of the given pitch, according to the given key. +SongEditor.prototype.getDegreeForPitch = function(pitch, key) +{ + var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); + var pitchDegree = key.scale.degrees.length - 0.5; + + for (var i = 0; i < key.scale.degrees.length; i++) + { + if (key.scale.degrees[i] == pitchInOctave) + { + pitchDegree = i; + break; + } + else if (key.scale.degrees[i] > pitchInOctave) + { + pitchDegree = i - 0.5; + break; + } + } + + return pitchDegree; +} + + // Returns the row index where a note of the given pitch would be placed, // according to the given key. -SongEditor.prototype.getNoteRow = function(pitch, key) +SongEditor.prototype.getRowForPitch = function(pitch, key) { var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); var pitchDegree = key.scale.degrees.length - 0.5; @@ -10,7 +34,7 @@ SongEditor.prototype.getNoteRow = function(pitch, key) { var originalTonicPitch = key.tonicPitch; key.tonicPitch = 0; - degreeOffsetFromC = this.getNoteRow(originalTonicPitch, key); + degreeOffsetFromC = Math.ceil(this.getRowForPitch(originalTonicPitch, key)); key.tonicPitch = originalTonicPitch; } @@ -32,15 +56,15 @@ SongEditor.prototype.getNoteRow = function(pitch, key) } -// Returns the scale degree of the given row index. -SongEditor.prototype.getNoteForRow = function(row, key) +// Returns the pitch of the given row index, according to the given key. +SongEditor.prototype.getPitchForRow = function(row, key) { var degreeOffsetFromC = 0; if (key.tonicPitch != 0) { var originalTonicPitch = key.tonicPitch; key.tonicPitch = 0; - degreeOffsetFromC = this.getNoteRow(originalTonicPitch, key); + degreeOffsetFromC = Math.ceil(this.getRowForPitch(originalTonicPitch, key)); key.tonicPitch = originalTonicPitch; } @@ -63,8 +87,8 @@ SongEditor.prototype.getNotePosition = function(block, row, tick, duration) } -// Returns a color string matching the given row. -SongEditor.prototype.getColorForRow = function(row) +// Returns a color string matching the given scale degree. +SongEditor.prototype.getColorForDegree = function(degree) { var colors = [ @@ -76,5 +100,5 @@ SongEditor.prototype.getColorForRow = function(row) "#6b16fc", "#ed18a8" ]; - return colors[(row - (row % 1)) % 7]; + return colors[(degree - (degree % 1)) % 7]; } diff --git a/src/editor_representation.js b/src/editor_representation.js index 45ff808..910eaae 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -134,7 +134,7 @@ SongEditor.prototype.refreshRepresentation = function() for (var n = 0; n < this.songData.notes.length; n++) { var note = this.songData.notes[n]; - var noteRow = this.getNoteRow(note.pitch, block.key); + var noteRow = this.getRowForPitch(note.pitch, block.key); var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); block.notes.push( { diff --git a/test.js b/test.js index 13de4b1..aec000a 100644 --- a/test.js +++ b/test.js @@ -5,8 +5,8 @@ function testOnLoad() song.addMeterChange(new SongDataMeterChange(0, 4, 4)); song.addKeyChange(new SongDataKeyChange(100, scaleMajor, 2)); song.addMeterChange(new SongDataMeterChange(150, 3, 4)); - song.addKeyChange(new SongDataKeyChange(200, scaleMixolydian, 0)); - song.addKeyChange(new SongDataKeyChange(300, scalePhrygianDominant, 0)); + song.addKeyChange(new SongDataKeyChange(200, scaleMixolydian, 11)); + song.addKeyChange(new SongDataKeyChange(300, scalePhrygianDominant, 4)); song.addNote(new SongDataNote(0, 300, 12)); song.addNote(new SongDataNote(0, 300, 5)); From 638eaeca6ad0da0c060daac0896f84cb382c439a Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Fri, 27 Nov 2015 10:04:31 -0200 Subject: [PATCH 07/46] relocate theory functions; draw striped pattern on fractional degree notes --- src/editor.js | 21 +++- src/editor_drawing.js | 211 +++++++++++++++++++++++------------ src/editor_helpers.js | 104 ----------------- src/editor_interaction.js | 4 +- src/editor_representation.js | 22 +++- src/songdata.js | 64 +++++++---- src/theory.js | 155 ++++++++++++++++++++++++- test.html | 1 - test.js | 8 +- 9 files changed, 379 insertions(+), 211 deletions(-) delete mode 100644 src/editor_helpers.js diff --git a/src/editor.js b/src/editor.js index f1f4d91..7af5c21 100644 --- a/src/editor.js +++ b/src/editor.js @@ -5,37 +5,49 @@ function SongEditor(canvas, songData) this.canvasWidth = parseFloat(canvas.width); this.canvasHeight = parseFloat(canvas.height); + // Set up mouse interaction. var that = this; this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; - this.songData = songData; - + // For each object in the song data, these arrays store a boolean + // indicating whether the respective object is currently selected in the editor. this.noteSelections = []; this.chordSelections = []; this.keyChangeSelections = []; this.meterChangeSelections = []; + // The scaling from ticks to pixels. this.tickZoom = 1; + + // The tick grid which the cursor is snapped to. this.tickSnap = 5; + + // These arrays store representation objects, which tell things like + // the object positioning/layout, and where they can be interacted with the mouse. this.viewBlocks = []; this.viewNotes = []; this.viewChords = []; this.viewKeyChanges = []; this.viewMeterChanges = []; + + // These control mouse interaction. this.mouseDown = false; this.mouseDragAction = null; this.mouseDragCurrent = { x: 0, y: 0 }; this.mouseDragOrigin = { x: 0, y: 0 }; this.mouseStretchPivotTick = 0; this.mouseStretchOriginTick = 0; + + // These indicate which object the mouse is currently hovering over. this.hoverNote = -1; this.hoverKeyChange = -1; this.hoverMeterChange = -1; this.hoverStretchR = false; this.hoverStretchL = false; + // Layout constants. this.WHOLE_NOTE_DURATION = 100; this.MARGIN_LEFT = 4; this.MARGIN_RIGHT = 4; @@ -45,16 +57,19 @@ function SongEditor(canvas, songData) this.NOTE_HEIGHT = 14; this.NOTE_MARGIN_HOR = 0.5; this.NOTE_MARGIN_VER = 0.5; + this.NOTE_STRETCH_MARGIN = 2; this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; this.KEYCHANGE_BAR_WIDTH = 20; this.METERCHANGE_BAR_WIDTH = 4; - this.refreshRepresentation(); + // Finally, set the song data. + this.setData(songData); } SongEditor.prototype.setData = function(songData) { this.songData = songData; + this.refreshRepresentation(); } \ No newline at end of file diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 9db0f4c..537b68d 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -1,61 +1,18 @@ -SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hovering, selected) +SongEditor.prototype.refreshCanvas = function() { - var block = this.viewBlocks[blockIndex]; - - if (tick + duration <= block.tick || - tick >= block.tick + block.duration) - return; - - var deg = this.getDegreeForPitch(pitch, block.key); - var row = this.getRowForPitch(pitch, block.key); - var pos = this.getNotePosition(block, row, tick, duration); - var col = this.getColorForDegree(deg); - this.ctx.save(); - this.ctx.fillStyle = col; - if (selected) - { - this.ctx.globalAlpha = 0.5; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, 3); - this.ctx.fillRect(pos.x1, pos.y2 - 3, pos.x2 - pos.x1, 3); - } - else if (hovering) - { - this.ctx.globalAlpha = 0.5; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - } - else - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - - this.ctx.globalAlpha = 0.5; - if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) - { - var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = this.getRowForPitch(pitch, nextBlock.key); - - var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; - var nextY2 = nextY1 + this.NOTE_HEIGHT; - - this.ctx.beginPath(); - this.ctx.moveTo(block.x2, pos.y1); - this.ctx.lineTo(nextBlock.x1, nextY1); - this.ctx.lineTo(nextBlock.x1, nextY2); - this.ctx.lineTo(block.x2, pos.y2); - this.ctx.fill(); - } - this.ctx.restore(); -} - - -SongEditor.prototype.refreshCanvas = function() -{ + // Clear background. this.ctx.fillStyle = "white"; this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); + // Draw blocks. + var BLOCK_BORDER_COLOR = "#000000"; + var BLOCK_MEASURE_COLOR = "#dddddd"; + var BLOCK_MEASURE_COLOR_STRONG = "#bbbbbb"; + for (var i = 0; i < this.viewBlocks.length; i++) { var block = this.viewBlocks[i]; @@ -63,7 +20,7 @@ SongEditor.prototype.refreshCanvas = function() // Draw rows. for (var row = 0; row < 14; row++) { - this.ctx.strokeStyle = (this.getPitchForRow(row, block.key) == block.key.tonicPitch ? "#bbbbbb" : "#dddddd"); + this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key) == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(block.x1, block.y2 - row * this.NOTE_HEIGHT); @@ -77,11 +34,14 @@ SongEditor.prototype.refreshCanvas = function() { if (n >= 0) { - this.ctx.strokeStyle = (submeasureCount == 0 ? "#bbbbbb" : "#dddddd"); + this.ctx.strokeStyle = (submeasureCount == 0 ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y1); this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); + + this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y2 + this.CHORDNOTE_MARGIN); + this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2 + this.CHORDNOTE_MARGIN + this.CHORD_HEIGHT); this.ctx.stroke(); } @@ -104,7 +64,7 @@ SongEditor.prototype.refreshCanvas = function() } // Draw borders. - this.ctx.strokeStyle = "black"; + this.ctx.strokeStyle = BLOCK_BORDER_COLOR; this.ctx.lineWidth = 2; var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); @@ -118,6 +78,11 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.textAlign = "left"; this.ctx.textBaseline = "top"; + var KEY_BORDER_COLOR = "#aaaaaa"; + var KEY_BORDER_COLOR_H = "#cccccc"; + var KEY_FILL_COLOR_SEL = "#eeeeee"; + var KEY_PITCH_COLOR = "#444444"; + for (var i = 0; i < this.viewKeyChanges.length; i++) { var keyChange = this.viewKeyChanges[i]; @@ -129,18 +94,20 @@ SongEditor.prototype.refreshCanvas = function() { var draggedKeyChange = this.getKeyChangeDragged(keyChange, this.mouseDragCurrent); + // Draw dragging-but-not-moved. if (draggedKeyChange.tick == keyChange.tick) { - this.ctx.fillStyle = "#eeeeee"; + this.ctx.fillStyle = KEY_FILL_COLOR_SEL; this.ctx.fillRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - this.ctx.strokeStyle = "#aaaaaa"; + this.ctx.strokeStyle = KEY_BORDER_COLOR; this.ctx.lineWidth = 2; this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); } + // Draw dragging. else { var x = this.getPositionForTick(draggedKeyChange.tick); - this.ctx.strokeStyle = "#aaaaaa"; + this.ctx.strokeStyle = KEY_BORDER_COLOR; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(x, keyChange.y1); @@ -148,36 +115,40 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.stroke(); } } + // Draw selected. else { - this.ctx.fillStyle = "#eeeeee"; + this.ctx.fillStyle = KEY_FILL_COLOR_SEL; this.ctx.fillRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - this.ctx.strokeStyle = "#aaaaaa"; + this.ctx.strokeStyle = KEY_BORDER_COLOR; this.ctx.lineWidth = 2; this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); } } + // Draw idle/hover. else { - this.ctx.strokeStyle = (this.hoverKeyChange == i ? "#cccccc" : "#aaaaaa"); + this.ctx.strokeStyle = (this.hoverKeyChange == i ? KEY_BORDER_COLOR_H : KEY_BORDER_COLOR); this.ctx.lineWidth = 2; this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); } + // Draw key name. this.ctx.font = "14px Tahoma"; var songKeyChange = this.songData.keyChanges[keyChange.keyChangeIndex]; - this.ctx.fillStyle = "#aaaaaa"; + this.ctx.fillStyle = KEY_BORDER_COLOR; this.ctx.fillText( - "" + theoryNoteName(songKeyChange.tonicPitch) + " " + songKeyChange.scale.name, + "" + theory.getNameForPitch(songKeyChange.tonicPitch, songKeyChange.scale) + " " + songKeyChange.scale.name, textX, keyChange.y1); + // Draw pitches. this.ctx.font = "10px Tahoma"; for (var row = 0; row < 14; row++) { - this.ctx.fillStyle = "#444444"; + this.ctx.fillStyle = KEY_PITCH_COLOR; this.ctx.fillText( - "" + theoryNoteName(this.getPitchForRow(row, songKeyChange)), + "" + theory.getNameForPitch(theory.getPitchForRow(row, songKeyChange)), keyChange.x1 + 4, this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row + 1) * this.NOTE_HEIGHT); } @@ -188,6 +159,10 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.font = "14px Tahoma"; this.ctx.textAlign = "left"; + var METER_BORDER_COLOR = "#88aaaa"; + var METER_BORDER_COLOR_HOVER = "#bbdddd"; + var METER_FILL_COLOR_SEL = "#bbdddd"; + for (var i = 0; i < this.viewMeterChanges.length; i++) { var meterChange = this.viewMeterChanges[i]; @@ -199,18 +174,20 @@ SongEditor.prototype.refreshCanvas = function() { var draggedMeterChange = this.getMeterChangeDragged(meterChange, this.mouseDragCurrent); + // Draw dragging-but-not-moved. if (draggedMeterChange.tick == meterChange.tick) { - this.ctx.fillStyle = "#aaeeee"; + this.ctx.fillStyle = METER_FILL_COLOR_SEL; this.ctx.fillRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - this.ctx.strokeStyle = "#88aaaa"; + this.ctx.strokeStyle = METER_BORDER_COLOR; this.ctx.lineWidth = 2; this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); } + // Draw dragging. else { var x = this.getPositionForTick(draggedMeterChange.tick); - this.ctx.strokeStyle = "#88aaaa"; + this.ctx.strokeStyle = METER_BORDER_COLOR; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(x, meterChange.y1); @@ -218,27 +195,121 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.stroke(); } } + // Draw selected. else { - this.ctx.fillStyle = "#aaeeee"; + this.ctx.fillStyle = METER_FILL_COLOR_SEL; this.ctx.fillRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - this.ctx.strokeStyle = "#88aaaa"; + this.ctx.strokeStyle = METER_BORDER_COLOR; this.ctx.lineWidth = 2; this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); } } + // Draw idle/hover. else { - this.ctx.strokeStyle = (this.hovermeterChange == i ? "#99cccc" : "#88aaaa"); + this.ctx.strokeStyle = (this.hoverMeterChange == i ? METER_BORDER_COLOR_HOVER : METER_BORDER_COLOR); this.ctx.lineWidth = 2; this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); } + // Draw numbers. var songMeterChange = this.songData.meterChanges[meterChange.meterChangeIndex]; - this.ctx.fillStyle = "#88aaaa"; + this.ctx.fillStyle = METER_BORDER_COLOR; this.ctx.fillText( "" + songMeterChange.numerator + " / " + songMeterChange.denominator, textX, meterChange.y1); } + + + this.ctx.restore(); +} + + +SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hovering, selected) +{ + var block = this.viewBlocks[blockIndex]; + + // Check if the note is inside the block. + if (tick + duration <= block.tick || + tick >= block.tick + block.duration) + return; + + var deg = theory.getDegreeForPitch(pitch, block.key); + var row = theory.getRowForPitch(pitch, block.key); + var pos = this.getNotePosition(block, row, tick, duration); + + this.drawDegreeColoredRectangle(deg, pos); + + this.ctx.save(); + + if (selected) + { + this.ctx.globalAlpha = 0.3; + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.fillRect(pos.x1, pos.y1 + 3, pos.x2 - pos.x1, pos.y2 - pos.y1 - 6); + } + else if (hovering) + { + this.ctx.globalAlpha = 0.3; + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + } + + this.ctx.restore(); + + // Draw possibly-bent part between blocks. + if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) + { + var nextBlock = this.viewBlocks[blockIndex + 1]; + var nextRow = theory.getRowForPitch(pitch, nextBlock.key); + + var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; + var nextY2 = nextY1 + this.NOTE_HEIGHT; + + var col = theory.getColorForDegree(deg - (deg % 1)); + + this.ctx.save(); + this.ctx.globalAlpha = 0.5; + this.ctx.fillStyle = col; + this.ctx.beginPath(); + this.ctx.moveTo(block.x2, pos.y1); + this.ctx.lineTo(nextBlock.x1, nextY1); + this.ctx.lineTo(nextBlock.x1, nextY2); + this.ctx.lineTo(block.x2, pos.y2); + this.ctx.fill(); + this.ctx.restore(); + } +} + + +SongEditor.prototype.drawDegreeColoredRectangle = function(deg, pos) +{ + var col = theory.getColorForDegree(deg - (deg % 1)); + + this.ctx.save(); + this.ctx.beginPath(); + this.ctx.rect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.clip(); + + this.ctx.fillStyle = col; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + + // Draw stripes on fractional scale degrees. + if ((deg % 1) != 0) + { + this.ctx.strokeStyle = theory.getColorForDegree((deg - (deg % 1) + 1) % 7); + this.ctx.lineWidth = 5; + for (var i = 0; i < pos.x2 - pos.x1 + 30; i += 15) + { + this.ctx.beginPath(); + this.ctx.moveTo(pos.x1 + i, pos.y1 - 5); + this.ctx.lineTo(pos.x1 + i - 20, pos.y1 - 5 + 20); + this.ctx.stroke(); + } + } + + this.ctx.restore(); } \ No newline at end of file diff --git a/src/editor_helpers.js b/src/editor_helpers.js deleted file mode 100644 index 58d9ada..0000000 --- a/src/editor_helpers.js +++ /dev/null @@ -1,104 +0,0 @@ -// Returns the scale degree of the given pitch, according to the given key. -SongEditor.prototype.getDegreeForPitch = function(pitch, key) -{ - var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); - var pitchDegree = key.scale.degrees.length - 0.5; - - for (var i = 0; i < key.scale.degrees.length; i++) - { - if (key.scale.degrees[i] == pitchInOctave) - { - pitchDegree = i; - break; - } - else if (key.scale.degrees[i] > pitchInOctave) - { - pitchDegree = i - 0.5; - break; - } - } - - return pitchDegree; -} - - -// Returns the row index where a note of the given pitch would be placed, -// according to the given key. -SongEditor.prototype.getRowForPitch = function(pitch, key) -{ - var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); - var pitchDegree = key.scale.degrees.length - 0.5; - - var degreeOffsetFromC = 0; - if (key.tonicPitch != 0) - { - var originalTonicPitch = key.tonicPitch; - key.tonicPitch = 0; - degreeOffsetFromC = Math.ceil(this.getRowForPitch(originalTonicPitch, key)); - key.tonicPitch = originalTonicPitch; - } - - for (var i = 0; i < key.scale.degrees.length; i++) - { - if (key.scale.degrees[i] == pitchInOctave) - { - pitchDegree = i; - break; - } - else if (key.scale.degrees[i] > pitchInOctave) - { - pitchDegree = i - 0.5; - break; - } - } - - return pitchDegree + degreeOffsetFromC + (Math.floor((pitch - key.tonicPitch) / 12) * key.scale.degrees.length); -} - - -// Returns the pitch of the given row index, according to the given key. -SongEditor.prototype.getPitchForRow = function(row, key) -{ - var degreeOffsetFromC = 0; - if (key.tonicPitch != 0) - { - var originalTonicPitch = key.tonicPitch; - key.tonicPitch = 0; - degreeOffsetFromC = Math.ceil(this.getRowForPitch(originalTonicPitch, key)); - key.tonicPitch = originalTonicPitch; - } - - return key.scale.degrees[(row + key.scale.degrees.length - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch; -} - - -// Returns the bounds of the given row's note's representation rectangle. -SongEditor.prototype.getNotePosition = function(block, row, tick, duration) -{ - var blockTick = tick - block.tick; - return { - resizeHandleL: block.x1 + blockTick * this.tickZoom, - resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, - x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), - x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), - y1: block.y2 - (row + 1) * this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, - y2: block.y2 - (row) * this.NOTE_HEIGHT - this.NOTE_MARGIN_VER, - }; -} - - -// Returns a color string matching the given scale degree. -SongEditor.prototype.getColorForDegree = function(degree) -{ - var colors = - [ - "#fb3214", - "#f7a610", - "#f2f201", - "#82f21e", - "#01beca", - "#6b16fc", - "#ed18a8" - ]; - return colors[(degree - (degree % 1)) % 7]; -} diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 5ea1c04..7bbe90b 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -187,12 +187,12 @@ SongEditor.prototype.handleMouseMove = function(ev) { this.hoverNote = note.noteIndex; - if (mousePos.x <= note.resizeHandleL + 2) + if (mousePos.x <= note.resizeHandleL + this.NOTE_STRETCH_MARGIN) { this.canvas.style.cursor = "ew-resize"; this.hoverStretchL = true; } - else if (mousePos.x >= note.resizeHandleR - 2) + else if (mousePos.x >= note.resizeHandleR - this.NOTE_STRETCH_MARGIN) { this.canvas.style.cursor = "ew-resize"; this.hoverStretchR = true; diff --git a/src/editor_representation.js b/src/editor_representation.js index 910eaae..5c58fc5 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -37,7 +37,7 @@ SongEditor.prototype.refreshRepresentation = function() { tick: 0, duration: 0, - key: new SongDataKeyChange(0, scaleMajor, 0), + key: new SongDataKeyChange(0, theory.scales[0], 0), meter: new SongDataMeterChange(0, 4, 4), notes: [], x1: x, @@ -131,10 +131,11 @@ SongEditor.prototype.refreshRepresentation = function() block.duration = nextChangeTick - block.tick; block.x2 = blockX2; + // Add notes' representations. for (var n = 0; n < this.songData.notes.length; n++) { var note = this.songData.notes[n]; - var noteRow = this.getRowForPitch(note.pitch, block.key); + var noteRow = theory.getRowForPitch(note.pitch, block.key); var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); block.notes.push( { @@ -171,10 +172,25 @@ SongEditor.prototype.refreshRepresentation = function() curBlock++; } - // Apply key/meter changes to the following block. + // Apply key/meter changes to the next block. if (nextIsWhat == NEXT_IS_KEYCHANGE) this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; else if (nextIsWhat == NEXT_IS_METERCHANGE) this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; } +} + + +// Returns the bounds of the given row's note's representation rectangle. +SongEditor.prototype.getNotePosition = function(block, row, tick, duration) +{ + var blockTick = tick - block.tick; + return { + resizeHandleL: block.x1 + blockTick * this.tickZoom, + resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, + x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), + x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), + y1: block.y2 - (row + 1) * this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, + y2: block.y2 - (row) * this.NOTE_HEIGHT - this.NOTE_MARGIN_VER, + }; } \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js index 248df13..0f6f3e9 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -1,23 +1,3 @@ -var C = 0; -var Cs = 1; -var D = 2; -var Ds = 3; -var E = 4; -var F = 5; -var Fs = 6; -var G = 7; -var Gs = 8; -var A = 9; -var As = 10; -var B = 11; - -var scaleMajor = { name: "Major", degrees: [ C, D, E, F, G, A, B ] }; -var scaleDorian = { name: "Dorian", degrees: [ C, D, Ds, F, G, A, As ] }; -var scaleMixolydian = { name: "Mixolydian", degrees: [ C, D, E, F, G, A, As ] }; -var scaleNaturalMinor = { name: "Natural Minor", degrees: [ C, D, Ds, F, G, Gs, As ] }; -var scalePhrygianDominant = { name: "Phrygian Dominant", degrees: [ C, Cs, E, F, G, Gs, As ] }; - - function SongData() { this.beatsPerMinute = 120; @@ -38,6 +18,14 @@ function SongDataNote(tick, duration, pitch) } +function SongDataChord(tick, duration, chord) +{ + this.tick = tick; + this.duration = duration; + this.chord = chord; +} + + function SongDataKeyChange(tick, scale, tonicPitch) { this.tick = tick; @@ -112,6 +100,29 @@ SongData.prototype.canAddNote = function(note) } +// Returns whether it is valid for the given chord to be added to the data. +SongData.prototype.canAddChord = function(chord) +{ + // Check for invalid values. + if (!this.isValidTick(chord.tick) || + !this.isValidDuration(chord.duration)) + return false; + + // Check whether the given chord collides with any chords already in data. + // TODO: Use binary search to avoid iterating through the entire array. + for (var i = 0; i < this.chord.length; i++) + { + var otherChord = this.chord[i]; + + if (otherChord.tick < chord.tick + chord.duration && + otherChord.tick + otherChord.duration > chord.tick) + return false; + } + + return true; +} + + // Returns whether it is valid for the given key change to be added to the data. SongData.prototype.canAddKeyChange = function(keyChange) { @@ -167,6 +178,19 @@ SongData.prototype.addNote = function(note) } +// Adds the given chord to the data, and returns whether it was successful. +SongData.prototype.addChord = function(chord) +{ + if (this.canAddChord(chord)) + { + arrayAddSortedByTick(this.chords, chord); + return true; + } + + return false; +} + + // Adds the given key change to the data, and returns whether it was successful. SongData.prototype.addKeyChange = function(keyChange) { diff --git a/src/theory.js b/src/theory.js index 6488cb3..81e9427 100644 --- a/src/theory.js +++ b/src/theory.js @@ -1,5 +1,152 @@ -function theoryNoteName(pitch, scale) +var theory = new Theory(); + + +function Theory() { - var notes = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; - return notes[pitch % 12]; -} \ No newline at end of file + var C = 0; + var Cs = 1; + var D = 2; + var Ds = 3; + var E = 4; + var F = 5; + var Fs = 6; + var G = 7; + var Gs = 8; + var A = 9; + var As = 10; + var B = 11; + + + this.C = C; + this.Cs = Cs; + this.D = D; + this.Ds = Ds; + this.E = E; + this.F = F; + this.Fs = Fs; + this.G = G; + this.Gs = Gs; + this.A = A; + this.As = As; + this.B = B; + + + this.scales = + [ + { name: "Major", degrees: [ C, D, E, F, G, A, B ] }, + { name: "Dorian", degrees: [ C, D, Ds, F, G, A, As ] }, + { name: "Mixolydian", degrees: [ C, D, E, F, G, A, As ] }, + { name: "Natural Minor", degrees: [ C, D, Ds, F, G, Gs, As ] }, + { name: "Phrygian Dominant", degrees: [ C, Cs, E, F, G, Gs, As ] } + ]; + + + this.chords = + [ + { name: "Major", roman: "I", pitches: [ C, E, G ] }, + { name: "Minor", roman: "i", pitches: [ C, Ds, G ] }, + { name: "Diminished", roman: "i^o", pitches: [ C, Ds, Fs ] }, + { name: "Augmented", roman: "I^+", pitches: [ C, E, Gs ] } + ]; + + + this.getNameForPitch = function(pitch, scale) + { + // TODO: Take the scale also into consideration. + var notes = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; + return notes[pitch % 12]; + }; + + + this.getColorForDegree = function(degree) + { + var colors = + [ + "#ff0000", + "#ffb014", + "#efe600", + "#00d300", + "#4800ff", + "#b800e5", + "#ff00cb" + ]; + return colors[degree]; + }; + + + // Returns the scale degree of the given pitch, according to the given key. + // May return fractional values, which indicates that the pitch falls + // between scale degrees. + this.getDegreeForPitch = function(pitch, key) + { + var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); + var pitchDegree = key.scale.degrees.length - 0.5; + + for (var i = 0; i < key.scale.degrees.length; i++) + { + if (key.scale.degrees[i] == pitchInOctave) + { + pitchDegree = i; + break; + } + else if (key.scale.degrees[i] > pitchInOctave) + { + pitchDegree = i - 0.5; + break; + } + } + + return pitchDegree; + }; + + + // Returns the row index where a note of the given pitch would be placed, + // according to the given key. May return fractional values, which + // indicates that the pitch falls between scale degrees. + this.getRowForPitch = function(pitch, key) + { + var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); + var pitchDegree = key.scale.degrees.length - 0.5; + + var degreeOffsetFromC = 0; + if (key.tonicPitch != 0) + { + var originalTonicPitch = key.tonicPitch; + key.tonicPitch = 0; + degreeOffsetFromC = Math.floor(this.getRowForPitch(originalTonicPitch, key)); + key.tonicPitch = originalTonicPitch; + } + + for (var i = 0; i < key.scale.degrees.length; i++) + { + if (key.scale.degrees[i] == pitchInOctave) + { + pitchDegree = i; + break; + } + else if (key.scale.degrees[i] > pitchInOctave) + { + pitchDegree = i - 0.5; + break; + } + } + + return pitchDegree + degreeOffsetFromC + (Math.floor((pitch - key.tonicPitch) / 12) * key.scale.degrees.length); + }; + + + // Returns the pitch of the given row index, according to the given key. + this.getPitchForRow = function(row, key) + { + var degreeOffsetFromC = 0; + if (key.tonicPitch != 0) + { + var originalTonicPitch = key.tonicPitch; + key.tonicPitch = 0; + degreeOffsetFromC = Math.floor(this.getRowForPitch(originalTonicPitch, key)); + key.tonicPitch = originalTonicPitch; + } + + return key.scale.degrees[(row + key.scale.degrees.length - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch; + }; +} diff --git a/test.html b/test.html index 8fa55b2..e3ccba8 100644 --- a/test.html +++ b/test.html @@ -15,7 +15,6 @@ - diff --git a/test.js b/test.js index aec000a..94b7f41 100644 --- a/test.js +++ b/test.js @@ -1,12 +1,12 @@ function testOnLoad() { var song = new SongData(); - song.addKeyChange(new SongDataKeyChange(0, scaleMajor, 0)); + song.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); song.addMeterChange(new SongDataMeterChange(0, 4, 4)); - song.addKeyChange(new SongDataKeyChange(100, scaleMajor, 2)); + song.addKeyChange(new SongDataKeyChange(100, theory.scales[1], theory.D)); song.addMeterChange(new SongDataMeterChange(150, 3, 4)); - song.addKeyChange(new SongDataKeyChange(200, scaleMixolydian, 11)); - song.addKeyChange(new SongDataKeyChange(300, scalePhrygianDominant, 4)); + song.addKeyChange(new SongDataKeyChange(200, theory.scales[2], theory.B)); + song.addKeyChange(new SongDataKeyChange(300, theory.scales[3], theory.E)); song.addNote(new SongDataNote(0, 300, 12)); song.addNote(new SongDataNote(0, 300, 5)); From 77c61d9c56af17e477107aafa2289e05d00a473f Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 28 Nov 2015 22:47:09 -0200 Subject: [PATCH 08/46] add chords; add cursor; start toolbox; make moving objects clip those already in place --- src/editor.js | 24 ++++- src/editor_drawing.js | 147 +++++++++++++++++++++++++-- src/editor_interaction.js | 192 +++++++++++++++++++++++++++++++++-- src/editor_representation.js | 74 +++++++++++--- src/songdata.js | 163 ++++++++++++----------------- src/theory.js | 45 +++++++- src/toolbox.js | 157 ++++++++++++++++++++++++++++ test.html | 5 +- test.js | 10 +- 9 files changed, 681 insertions(+), 136 deletions(-) create mode 100644 src/toolbox.js diff --git a/src/editor.js b/src/editor.js index 7af5c21..2f7e5fc 100644 --- a/src/editor.js +++ b/src/editor.js @@ -11,6 +11,9 @@ function SongEditor(canvas, songData) this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; + // Set up callback arrays. + this.cursorChangedCallbacks = []; + // For each object in the song data, these arrays store a boolean // indicating whether the respective object is currently selected in the editor. this.noteSelections = []; @@ -32,6 +35,10 @@ function SongEditor(canvas, songData) this.viewKeyChanges = []; this.viewMeterChanges = []; + // These control cursor (the vertical blue bar) interaction. + this.cursorTick = 0; + this.showCursor = true; + // These control mouse interaction. this.mouseDown = false; this.mouseDragAction = null; @@ -42,6 +49,7 @@ function SongEditor(canvas, songData) // These indicate which object the mouse is currently hovering over. this.hoverNote = -1; + this.hoverChord = -1; this.hoverKeyChange = -1; this.hoverMeterChange = -1; this.hoverStretchR = false; @@ -52,7 +60,7 @@ function SongEditor(canvas, songData) this.MARGIN_LEFT = 4; this.MARGIN_RIGHT = 4; this.MARGIN_TOP = 4; - this.MARGIN_BOTTOM = 4; + this.MARGIN_BOTTOM = 8; this.HEADER_MARGIN = 40; this.NOTE_HEIGHT = 14; this.NOTE_MARGIN_HOR = 0.5; @@ -60,6 +68,7 @@ function SongEditor(canvas, songData) this.NOTE_STRETCH_MARGIN = 2; this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; + this.CHORD_ORNAMENT_HEIGHT = 5; this.KEYCHANGE_BAR_WIDTH = 20; this.METERCHANGE_BAR_WIDTH = 4; @@ -72,4 +81,17 @@ SongEditor.prototype.setData = function(songData) { this.songData = songData; this.refreshRepresentation(); +} + + +SongEditor.prototype.addOnCursorChanged = function(func) +{ + this.cursorChangedCallbacks.push(func); +} + + +SongEditor.prototype.callOnCursorChanged = function() +{ + for (var i = 0; i < this.cursorChangedCallbacks.length; i++) + this.cursorChangedCallbacks[i](); } \ No newline at end of file diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 537b68d..5e3e03f 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -9,6 +9,7 @@ SongEditor.prototype.refreshCanvas = function() // Draw blocks. + var CURSOR_COLOR = "#0000ff"; var BLOCK_BORDER_COLOR = "#000000"; var BLOCK_MEASURE_COLOR = "#dddddd"; var BLOCK_MEASURE_COLOR_STRONG = "#bbbbbb"; @@ -23,8 +24,8 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key) == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); this.ctx.lineWidth = 2; this.ctx.beginPath(); - this.ctx.moveTo(block.x1, block.y2 - row * this.NOTE_HEIGHT); - this.ctx.lineTo(block.x2, block.y2 - row * this.NOTE_HEIGHT); + this.ctx.moveTo(block.x1, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - row * this.NOTE_HEIGHT); + this.ctx.lineTo(block.x2, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - row * this.NOTE_HEIGHT); this.ctx.stroke(); } @@ -38,10 +39,10 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y1); - this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); + this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN); - this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y2 + this.CHORDNOTE_MARGIN); - this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2 + this.CHORDNOTE_MARGIN + this.CHORD_HEIGHT); + this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT); + this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); this.ctx.stroke(); } @@ -63,13 +64,59 @@ SongEditor.prototype.refreshCanvas = function() this.drawNote(i, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); } + // Draw chord. + for (var n = 0; n < block.chords.length; n++) + { + var chordIndex = block.chords[n].chordIndex; + var chord = this.songData.chords[chordIndex]; + + if (this.chordSelections[chordIndex] && this.mouseDragAction != null) + { + var draggedChord = this.getChordDragged(chord, this.mouseDragCurrent); + this.drawChord(i, chord, draggedChord.tick, draggedChord.duration, chordIndex == this.hoverChord, true); + } + else + this.drawChord(i, chord, chord.tick, chord.duration, chordIndex == this.hoverChord, this.chordSelections[chordIndex]); + } + // Draw borders. this.ctx.strokeStyle = BLOCK_BORDER_COLOR; this.ctx.lineWidth = 2; var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); - this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - block.y1); - this.ctx.strokeRect(block.x1, block.y2 + this.CHORDNOTE_MARGIN, x2 - block.x1, this.CHORD_HEIGHT); + this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - block.y1); + this.ctx.strokeRect(block.x1, block.y2 - this.CHORD_HEIGHT, x2 - block.x1, this.CHORD_HEIGHT); + + // Draw cursor. + if (this.showCursor && this.cursorTick >= block.tick && this.cursorTick < block.tick + block.duration) + { + this.ctx.strokeStyle = CURSOR_COLOR; + this.ctx.fillStyle = CURSOR_COLOR; + this.ctx.lineWidth = 2; + + var cursorX = block.x1 + (this.cursorTick - block.tick) * this.tickZoom; + var cursorY1 = block.y1; + var cursorY2 = block.y2; + + this.ctx.beginPath(); + this.ctx.moveTo(cursorX, cursorY1); + this.ctx.lineTo(cursorX, cursorY2); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(cursorX, cursorY1); + this.ctx.lineTo(cursorX - 6, cursorY1 - 6); + this.ctx.lineTo(cursorX + 6, cursorY1 - 6); + this.ctx.lineTo(cursorX, cursorY1); + this.ctx.fill(); + + this.ctx.beginPath(); + this.ctx.moveTo(cursorX, cursorY2); + this.ctx.lineTo(cursorX - 6, cursorY2 + 6); + this.ctx.lineTo(cursorX + 6, cursorY2 + 6); + this.ctx.lineTo(cursorX, cursorY2); + this.ctx.fill(); + } } @@ -144,7 +191,7 @@ SongEditor.prototype.refreshCanvas = function() // Draw pitches. this.ctx.font = "10px Tahoma"; - for (var row = 0; row < 14; row++) + for (var row = 0; row < 13; row++) { this.ctx.fillStyle = KEY_PITCH_COLOR; this.ctx.fillText( @@ -242,6 +289,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove this.drawDegreeColoredRectangle(deg, pos); + // Draw highlights. this.ctx.save(); if (selected) @@ -266,7 +314,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove var nextBlock = this.viewBlocks[blockIndex + 1]; var nextRow = theory.getRowForPitch(pitch, nextBlock.key); - var nextY1 = nextBlock.y2 - (nextRow + 1) * this.NOTE_HEIGHT; + var nextY1 = nextBlock.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (nextRow + 1) * this.NOTE_HEIGHT; var nextY2 = nextY1 + this.NOTE_HEIGHT; var col = theory.getColorForDegree(deg - (deg % 1)); @@ -285,6 +333,87 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove } +SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hovering, selected) +{ + var block = this.viewBlocks[blockIndex]; + + // Check if the note is inside the block. + if (tick + duration <= block.tick || + tick >= block.tick + block.duration) + return; + + var deg = theory.getDegreeForPitch(chord.rootPitch, block.key); + var pos = this.getChordPosition(block, tick, duration); + + this.drawDegreeColoredRectangle(deg, { x1: pos.x1, y1: pos.y1, x2: pos.x2, y2: pos.y1 + this.CHORD_ORNAMENT_HEIGHT }); + this.drawDegreeColoredRectangle(deg, { x1: pos.x1, y1: pos.y2 - this.CHORD_ORNAMENT_HEIGHT, x2: pos.x2, y2: pos.y2 }); + + // Draw roman symbol. + var numeral = theory.getRomanNumeralForPitch(chord.rootPitch, block.key); + var romanText = chord.chord.roman.replace("I", numeral).replace("i", numeral.toLowerCase()); + + this.ctx.fillStyle = "#000000"; + this.ctx.textAlign = "center"; + this.ctx.textBaseline = "middle"; + + this.ctx.font = "20px Tahoma"; + var supTextWidth = this.ctx.measureText(chord.chord.romanSup).width; + var subTextWidth = this.ctx.measureText(chord.chord.romanSub).width; + + this.ctx.font = "30px Tahoma"; + var mainTextWidth = this.ctx.measureText(romanText).width; + var totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; + + var maxTextWidth = pos.x2 - pos.x1 - 2; + if (totalTextWidth > maxTextWidth) + { + var proportion = totalTextWidth / maxTextWidth; + supTextWidth /= proportion; + subTextWidth /= proportion; + mainTextWidth /= proportion; + totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; + } + + this.ctx.fillText(romanText, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth / 2, (pos.y1 + pos.y2) / 2, maxTextWidth - supTextWidth - subTextWidth); + + this.ctx.font = "20px Tahoma"; + this.ctx.fillText(chord.chord.romanSup, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, (pos.y1 + pos.y2) / 2 - 10, maxTextWidth - mainTextWidth - subTextWidth); + + // Draw highlights. + this.ctx.save(); + + if (selected) + { + this.ctx.globalAlpha = 0.3; + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.fillRect(pos.x1, pos.y1 + 3, pos.x2 - pos.x1, pos.y2 - pos.y1 - 6); + } + else if (hovering) + { + this.ctx.globalAlpha = 0.3; + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + } + + this.ctx.restore(); + + // Draw possibly-bent part between blocks. + if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) + { + var nextBlock = this.viewBlocks[blockIndex + 1]; + var col = theory.getColorForDegree(deg - (deg % 1)); + + this.ctx.save(); + this.ctx.globalAlpha = 0.5; + this.ctx.fillStyle = col; + this.drawDegreeColoredRectangle(deg, { x1: block.x2, y1: pos.y1, x2: nextBlock.x1, y2: pos.y1 + this.CHORD_ORNAMENT_HEIGHT } ); + this.drawDegreeColoredRectangle(deg, { x1: block.x2, y1: pos.y2 - this.CHORD_ORNAMENT_HEIGHT, x2: nextBlock.x1, y2: pos.y2 }); + this.ctx.restore(); + } +} + + SongEditor.prototype.drawDegreeColoredRectangle = function(deg, pos) { var col = theory.getColorForDegree(deg - (deg % 1)); diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 7bbe90b..b7a63e4 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -3,6 +3,9 @@ SongEditor.prototype.unselectAll = function() for (var i = 0; i < this.noteSelections.length; i++) this.noteSelections[i] = false; + for (var i = 0; i < this.chordSelections.length; i++) + this.chordSelections[i] = false; + for (var i = 0; i < this.keyChangeSelections.length; i++) this.keyChangeSelections[i] = false; @@ -14,6 +17,7 @@ SongEditor.prototype.unselectAll = function() SongEditor.prototype.clearHover = function() { this.hoverNote = -1; + this.hoverChord = -1; this.hoverKeyChange = -1; this.hoverMeterChange = -1; this.hoverStretchR = false; @@ -61,7 +65,7 @@ SongEditor.prototype.getTickAtPosition = function(x) if (x >= block.x1 && (b == this.viewBlocks.length - 1 || x <= this.viewBlocks[b + 1].x1)) { - return Math.round((block.tick + Math.min(block.duration, Math.round((x - block.x1) / this.tickZoom))) / this.tickSnap) * this.tickSnap; + return Math.round((block.tick + Math.min(block.duration, Math.ceil((x - block.x1) / this.tickZoom))) / this.tickSnap) * this.tickSnap; } } @@ -69,31 +73,77 @@ SongEditor.prototype.getTickAtPosition = function(x) } +SongEditor.prototype.getKeyAtTick = function(tick) +{ + for (var b = 0; b < this.viewBlocks.length; b++) + { + var block = this.viewBlocks[b]; + + if (tick >= block.tick && tick < block.tick + block.duration) + { + return block.key; + } + } + + return new SongDataKeyChange(0, theory.scales[0], 0); +} + + SongEditor.prototype.getEarliestSelectedTick = function() { - // FIXME: Take chords and key/meter changes into consideration. + // FIXME: Take key/meter changes into consideration. // TODO: Use binary search. + var earliest = 1000000; for (var n = 0; n < this.noteSelections.length; n++) { if (this.noteSelections[n]) - return this.songData.notes[n].tick; + { + earliest = this.songData.notes[n].tick; + break; + } } - return 0; + for (var n = 0; n < this.chordSelections.length; n++) + { + if (this.chordSelections[n]) + { + if (this.songData.chords[n].tick < earliest) + return this.songData.chords[n].tick; + else if (this.songData.chords[n].tick >= earliest) + break; + } + } + + return earliest; } SongEditor.prototype.getLatestSelectedTick = function() { - // FIXME: Take chords and key/meter changes into consideration. + // FIXME: Take key/meter changes into consideration. // TODO: Use binary search. + var latest = 0; for (var n = this.noteSelections.length - 1; n >= 0; n--) { if (this.noteSelections[n]) - return this.songData.notes[n].tick + this.songData.notes[n].duration; + { + var tick = this.songData.notes[n].tick + this.songData.notes[n].duration; + if (tick < latest) + latest = tick; + } } - return 0; + for (var n = this.chordSelections.length - 1; n >= 0; n--) + { + if (this.chordSelections[n]) + { + var tick = this.songData.chords[n].tick + this.songData.chords[n].duration; + if (tick < latest) + latest = tick; + } + } + + return tick; } @@ -137,6 +187,43 @@ SongEditor.prototype.getNoteDragged = function(note, dragPosition) } +SongEditor.prototype.getChordDragged = function(chord, dragPosition) +{ + var dragTick = this.getTickAtPosition(dragPosition.x); + var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin.x); + + if (this.mouseDragAction == "move") + { + return { + tick: Math.max(0, chord.tick + tickOffset), + duration: chord.duration + }; + } + else if (this.mouseDragAction == "stretch") + { + var x1Proportion = (chord.tick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + var x2Proportion = (chord.tick + chord.duration - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + var mouseProportion = (dragTick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + + var pivotDist = (this.mouseStretchOriginTick - this.mouseStretchPivotTick); + var newX1 = Math.round((this.mouseStretchPivotTick + pivotDist * (x1Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; + var newX2 = Math.round((this.mouseStretchPivotTick + pivotDist * (x2Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; + + if (newX2 < newX1) + { + var temp = newX1; + newX1 = newX2; + newX2 = temp; + } + + return { + tick: Math.max(0, newX1), + duration: newX2 - newX1 + }; + } +} + + SongEditor.prototype.getKeyChangeDragged = function(keyChange, dragPosition) { var tickOffset = this.getTickAtPosition(dragPosition.x) - this.getTickAtPosition(this.mouseDragOrigin.x); @@ -175,11 +262,11 @@ SongEditor.prototype.handleMouseMove = function(ev) // Check what's under the mouse, if it's not down. if (!this.mouseDown) { - // Check for notes. for (var b = 0; b < this.viewBlocks.length; b++) { if (isPointInside(mousePos, this.viewBlocks[b])) { + // Check for notes. for (var n = 0; n < this.viewBlocks[b].notes.length; n++) { var note = this.viewBlocks[b].notes[n]; @@ -203,12 +290,41 @@ SongEditor.prototype.handleMouseMove = function(ev) break; } } + + // Check for chords. + if (this.hoverNote < 0) + { + for (var n = 0; n < this.viewBlocks[b].chords.length; n++) + { + var chord = this.viewBlocks[b].chords[n]; + if (isPointInside(mousePos, chord)) + { + this.hoverChord = chord.chordIndex; + + if (mousePos.x <= chord.resizeHandleL + this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchL = true; + } + else if (mousePos.x >= chord.resizeHandleR - this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchR = true; + } + else + this.canvas.style.cursor = "pointer"; + + break; + } + } + } + break; } } // Check for key changes. - if (this.hoverNote < 0) + if (this.hoverNote < 0 && this.hoverChord < 0) { for (var n = 0; n < this.viewKeyChanges.length; n++) { @@ -223,7 +339,7 @@ SongEditor.prototype.handleMouseMove = function(ev) } // Check for meter changes. - if (this.hoverNote < 0 && this.hoverKeyChange < 0) + if (this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) { for (var n = 0; n < this.viewMeterChanges.length; n++) { @@ -257,10 +373,15 @@ SongEditor.prototype.handleMouseDown = function(ev) this.mouseDragAction = null; this.mouseDragOrigin = mousePos; + this.cursorTick = this.getTickAtPosition(mousePos.x); + this.showCursor = true; + // Start a dragging operation. if (this.hoverNote >= 0) { + this.showCursor = false; this.noteSelections[this.hoverNote] = true; + if (this.hoverStretchR) { this.mouseDragAction = "stretch"; @@ -276,19 +397,42 @@ SongEditor.prototype.handleMouseDown = function(ev) else this.mouseDragAction = "move"; } + else if (this.hoverChord >= 0) + { + this.showCursor = false; + this.chordSelections[this.hoverChord] = true; + + if (this.hoverStretchR) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick + this.songData.chords[this.hoverChord].duration; + this.mouseStretchPivotTick = this.getEarliestSelectedTick(); + } + else if (this.hoverStretchL) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick; + this.mouseStretchPivotTick = this.getLatestSelectedTick(); + } + else + this.mouseDragAction = "move"; + } else if (this.hoverKeyChange >= 0) { + this.showCursor = false; this.keyChangeSelections[this.hoverKeyChange] = true; this.mouseDragAction = "move"; } else if (this.hoverMeterChange >= 0) { + this.showCursor = false; this.meterChangeSelections[this.hoverMeterChange] = true; this.mouseDragAction = "move"; } this.mouseDown = true; this.refreshCanvas(); + this.callOnCursorChanged(); } @@ -312,6 +456,16 @@ SongEditor.prototype.handleMouseUp = function(ev) } } + var selectedChords = []; + for (var n = this.chordSelections.length - 1; n >= 0; n--) + { + if (this.chordSelections[n]) + { + selectedChords.push(this.songData.chords[n]); + this.songData.chords.splice(n, 1); + } + } + var selectedKeyChanges = []; for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) { @@ -342,6 +496,15 @@ SongEditor.prototype.handleMouseUp = function(ev) this.songData.addNote(newNote); } + var newChords = []; + for (var n = 0; n < selectedChords.length; n++) + { + var draggedChord = this.getChordDragged(selectedChords[n], mousePos); + var newChord = new SongDataChord(draggedChord.tick, draggedChord.duration, selectedChords[n].chord, selectedChords[n].rootPitch); + newChords.push(newChord); + this.songData.addChord(newChord); + } + var newKeyChanges = []; for (var n = 0; n < selectedKeyChanges.length; n++) { @@ -373,6 +536,15 @@ SongEditor.prototype.handleMouseUp = function(ev) } } + for (var n = 0; n < this.songData.chords.length; n++) + { + for (var m = 0; m < newChords.length; m++) + { + if (this.songData.chords[n] == newChords[m]) + this.chordSelections[n] = true; + } + } + for (var n = 0; n < this.songData.keyChanges.length; n++) { for (var m = 0; m < newKeyChanges.length; m++) diff --git a/src/editor_representation.js b/src/editor_representation.js index 5c58fc5..db8c830 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -1,17 +1,24 @@ // Sets up representation objects for the data in the song, -// which are used to draw the staff and to interact with the mouse. +// which are used to draw the staff and for interaction with the mouse. SongEditor.prototype.refreshRepresentation = function() { + // Clear representation objects. this.viewBlocks = []; this.viewNotes = []; this.viewChords = []; this.viewKeyChanges = []; this.viewMeterChanges = []; + // Clear selection arrays and push a boolean false for each object in the song data, + // effectively unselecting everything, while also accomodating added/removed objects. this.noteSelections = []; for (var i = 0; i < this.songData.notes.length; i++) this.noteSelections.push(false); + this.chordSelections = []; + for (var i = 0; i < this.songData.chords.length; i++) + this.chordSelections.push(false); + this.keyChangeSelections = []; for (var i = 0; i < this.songData.keyChanges.length; i++) this.keyChangeSelections.push(false); @@ -20,11 +27,14 @@ SongEditor.prototype.refreshRepresentation = function() for (var i = 0; i < this.songData.meterChanges.length; i++) this.meterChangeSelections.push(false); + // Set up some layout constants. var blockY1 = this.MARGIN_TOP + this.HEADER_MARGIN; - var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM; var changeY1 = this.MARGIN_TOP; var chordY2 = this.canvasHeight - this.MARGIN_BOTTOM; + // Set up current block drawing position, current tick, and + // iterators through the song data. var x = this.MARGIN_LEFT; var tick = 0; var curNote = 0; @@ -32,6 +42,7 @@ SongEditor.prototype.refreshRepresentation = function() var curKeyChange = 0; var curMeterChange = 0; + // Set up the first block. var curBlock = 0; this.viewBlocks.push( { @@ -40,19 +51,22 @@ SongEditor.prototype.refreshRepresentation = function() key: new SongDataKeyChange(0, theory.scales[0], 0), meter: new SongDataMeterChange(0, 4, 4), notes: [], + chords: [], x1: x, y1: blockY1, x2: x, y2: blockY2 }); - var NEXT_IS_NONE = 0; - var NEXT_IS_KEYCHANGE = 1; - var NEXT_IS_METERCHANGE = 2; - + // Loop will break after the last block. while (true) { - // Find the tick where the current block ends. + // Identifiers for whether there's a following key or meter change. + var NEXT_IS_NONE = 0; + var NEXT_IS_KEYCHANGE = 1; + var NEXT_IS_METERCHANGE = 2; + + // Find the tick where the current block ends due to a key or meter change. var nextChangeTick = this.songData.lastTick; var nextIsWhat = NEXT_IS_NONE; @@ -76,11 +90,11 @@ SongEditor.prototype.refreshRepresentation = function() } } - // Advance draw position until the next tick. + // Advance draw position until the block's end tick. x += (nextChangeTick - tick) * this.tickZoom; tick = nextChangeTick; - // If there is a key change, add its visualization and advance its iterator. + // If there is a key change, add its representation and advance its iterator. var blockX2 = x; if (nextIsWhat == NEXT_IS_KEYCHANGE) @@ -99,7 +113,7 @@ SongEditor.prototype.refreshRepresentation = function() curKeyChange++; } - // Or if there is a meter change, add its visualization and advance its iterator. + // Or if there is a meter change, add its representation and advance its iterator. else if (nextIsWhat == NEXT_IS_METERCHANGE) { x += this.METERCHANGE_BAR_WIDTH; @@ -151,6 +165,25 @@ SongEditor.prototype.refreshRepresentation = function() }); } + // Add chords' representations. + for (var n = 0; n < this.songData.chords.length; n++) + { + var chord = this.songData.chords[n]; + var chordPos = this.getChordPosition(block, chord.tick, chord.duration); + block.chords.push( + { + chordIndex: n, + tick: chord.tick, + duration: chord.duration, + resizeHandleL: chordPos.resizeHandleL, + resizeHandleR: chordPos.resizeHandleR, + x1: chordPos.x1, + y1: chordPos.y1, + x2: chordPos.x2, + y2: chordPos.y2 + }); + } + // If this is the final block, we can stop now. if (nextIsWhat == NEXT_IS_NONE) break; @@ -163,6 +196,7 @@ SongEditor.prototype.refreshRepresentation = function() key: block.key, meter: block.meter, notes: [], + chords: [], x1: x, y1: blockY1, x2: x, @@ -175,6 +209,7 @@ SongEditor.prototype.refreshRepresentation = function() // Apply key/meter changes to the next block. if (nextIsWhat == NEXT_IS_KEYCHANGE) this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; + else if (nextIsWhat == NEXT_IS_METERCHANGE) this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; } @@ -190,7 +225,22 @@ SongEditor.prototype.getNotePosition = function(block, row, tick, duration) resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), - y1: block.y2 - (row + 1) * this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, - y2: block.y2 - (row) * this.NOTE_HEIGHT - this.NOTE_MARGIN_VER, + y1: block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row + 1) * this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, + y2: block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row) * this.NOTE_HEIGHT - this.NOTE_MARGIN_VER + }; +} + + +// Returns the bounds of the given chord's representation rectangle. +SongEditor.prototype.getChordPosition = function(block, tick, duration) +{ + var blockTick = tick - block.tick; + return { + resizeHandleL: block.x1 + blockTick * this.tickZoom, + resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, + x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), + x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), + y1: block.y2 - this.CHORD_HEIGHT, + y2: block.y2 }; } \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js index 0f6f3e9..352a1ce 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -18,11 +18,12 @@ function SongDataNote(tick, duration, pitch) } -function SongDataChord(tick, duration, chord) +function SongDataChord(tick, duration, chord, rootPitch) { this.tick = tick; this.duration = duration; this.chord = chord; + this.rootPitch = rootPitch; } @@ -76,142 +77,110 @@ SongData.prototype.isValidDuration = function(duration) } -// Returns whether it is valid for the given note to be added to the data. -SongData.prototype.canAddNote = function(note) +// Adds the given note to the data, and returns whether it was successful. +SongData.prototype.addNote = function(note) { - // Check for invalid values. - if (!this.isValidTick(note.tick) || - !this.isValidDuration(note.duration)) + if (!this.isValidTick(note.tick) || !this.isValidDuration(note.duration)) return false; - // Check whether the given note collides with any notes already in data. - // TODO: Use binary search to avoid iterating through the entire array. - for (var i = 0; i < this.notes.length; i++) + // Clip notes which collide with the new one. + // TODO: Split a note into two in case the new one is contained within it. + for (var i = this.notes.length - 1; i >= 0; i--) { var otherNote = this.notes[i]; - if (otherNote.pitch == note.pitch && - otherNote.tick < note.tick + note.duration && - otherNote.tick + otherNote.duration > note.tick) - return false; + if (otherNote.pitch != note.pitch) + continue; + + if (otherNote.tick >= note.tick && otherNote.tick + otherNote.duration <= note.tick + note.duration) + { + this.notes.splice(i, 1); + } + else if (otherNote.tick >= note.tick && otherNote.tick < note.tick + note.duration && otherNote.tick + otherNote.duration > note.tick + note.duration) + { + var tickEnd = otherNote.tick + otherNote.duration; + otherNote.tick = note.tick + note.duration; + otherNote.duration = tickEnd - otherNote.tick; + } + else if (otherNote.tick < note.tick && otherNote.tick + otherNote.duration >= note.tick) + { + otherNote.duration = note.tick - otherNote.tick; + } } + arrayAddSortedByTick(this.notes, note); return true; } -// Returns whether it is valid for the given chord to be added to the data. -SongData.prototype.canAddChord = function(chord) +// Adds the given chord to the data, and returns whether it was successful. +SongData.prototype.addChord = function(chord) { - // Check for invalid values. - if (!this.isValidTick(chord.tick) || - !this.isValidDuration(chord.duration)) + if (!this.isValidTick(chord.tick) || !this.isValidDuration(chord.duration)) return false; - // Check whether the given chord collides with any chords already in data. - // TODO: Use binary search to avoid iterating through the entire array. - for (var i = 0; i < this.chord.length; i++) + // Clip chords which collide with the new one. + // TODO: Split a chord into two in case the new one is contained within it. + for (var i = this.chords.length - 1; i >= 0; i--) { - var otherChord = this.chord[i]; + var otherChord = this.chords[i]; - if (otherChord.tick < chord.tick + chord.duration && - otherChord.tick + otherChord.duration > chord.tick) - return false; + if (otherChord.tick >= chord.tick && otherChord.tick + otherChord.duration <= chord.tick + chord.duration) + { + this.chords.splice(i, 1); + } + else if (otherChord.tick >= chord.tick && otherChord.tick < chord.tick + chord.duration && otherChord.tick + otherChord.duration > chord.tick + chord.duration) + { + var tickEnd = otherChord.tick + otherChord.duration; + otherChord.tick = chord.tick + chord.duration; + otherChord.duration = tickEnd - otherChord.tick; + } + else if (otherChord.tick < chord.tick && otherChord.tick + otherChord.duration >= chord.tick) + { + otherChord.duration = chord.tick - otherChord.tick; + } } + arrayAddSortedByTick(this.chords, chord); return true; } -// Returns whether it is valid for the given key change to be added to the data. -SongData.prototype.canAddKeyChange = function(keyChange) +// Adds the given key change to the data, and returns whether it was successful. +SongData.prototype.addKeyChange = function(keyChange) { - // Check for invalid values. if (!this.isValidTick(keyChange.tick)) return false; - // Check whether the given key change coincides with another already in data. - // TODO: Use binary search to avoid iterating through the entire array. - for (var i = 0; i < this.keyChanges.length; i++) + // Remove key changes which were at the same tick. + for (var i = this.keyChanges.length - 1; i >= 0; i--) { - var otherKeyChange = this.keyChanges[i]; - - if (otherKeyChange.tick == keyChange.tick) - return false; + if (this.keyChanges[i].tick == keyChange.tick) + { + this.keyChanges.splice(i, 1); + } } + arrayAddSortedByTick(this.keyChanges, keyChange); return true; } -// Returns whether it is valid for the given meter change to be added to the data. -SongData.prototype.canAddMeterChange = function(meterChange) +// Adds the given meter change to the data, and returns whether it was successful. +SongData.prototype.addMeterChange = function(meterChange) { - // Check for invalid values. if (!this.isValidTick(meterChange.tick)) return false; - // Check whether the given meter change coincides with another already in data. - // TODO: Use binary search to avoid iterating through the entire array. - for (var i = 0; i < this.meterChanges.length; i++) + // Remove meter changes which were at the same tick. + for (var i = this.meterChanges.length - 1; i >= 0; i--) { - var otherMeterChange = this.meterChanges[i]; - - if (otherMeterChange.tick == meterChange.tick) - return false; + if (this.meterChanges[i].tick == meterChange.tick) + { + this.meterChanges.splice(i, 1); + } } + arrayAddSortedByTick(this.meterChanges, meterChange); return true; -} - - -// Adds the given note to the data, and returns whether it was successful. -SongData.prototype.addNote = function(note) -{ - if (this.canAddNote(note)) - { - arrayAddSortedByTick(this.notes, note); - return true; - } - - return false; -} - - -// Adds the given chord to the data, and returns whether it was successful. -SongData.prototype.addChord = function(chord) -{ - if (this.canAddChord(chord)) - { - arrayAddSortedByTick(this.chords, chord); - return true; - } - - return false; -} - - -// Adds the given key change to the data, and returns whether it was successful. -SongData.prototype.addKeyChange = function(keyChange) -{ - if (this.canAddKeyChange(keyChange)) - { - arrayAddSortedByTick(this.keyChanges, keyChange); - return true; - } - - return false; -} - - -// Adds the given meter change to the data, and returns whether it was successful. -SongData.prototype.addMeterChange = function(meterChange) -{ - if (this.canAddMeterChange(meterChange)) - { - arrayAddSortedByTick(this.meterChanges, meterChange); - return true; - } - - return false; } \ No newline at end of file diff --git a/src/theory.js b/src/theory.js index 81e9427..a167ed6 100644 --- a/src/theory.js +++ b/src/theory.js @@ -43,10 +43,10 @@ function Theory() this.chords = [ - { name: "Major", roman: "I", pitches: [ C, E, G ] }, - { name: "Minor", roman: "i", pitches: [ C, Ds, G ] }, - { name: "Diminished", roman: "i^o", pitches: [ C, Ds, Fs ] }, - { name: "Augmented", roman: "I^+", pitches: [ C, E, Gs ] } + { name: "Major", roman: "I", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, + { name: "Minor", roman: "i", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, + { name: "Diminished", roman: "i", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, + { name: "Augmented", roman: "I", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] } ]; @@ -57,6 +57,14 @@ function Theory() return notes[pitch % 12]; }; + + this.getRomanNumeralForPitch = function(pitch, key) + { + // TODO: Take the scale also into consideration for naming. + var numerals = ["I", "♯I", "II", "♯II", "III", "IV", "♯IV", "V", "♯V", "VI", "♯VI", "VII"]; + return numerals[(pitch + 12 - key.tonicPitch) % 12]; + }; + this.getColorForDegree = function(degree) { @@ -149,4 +157,33 @@ function Theory() return key.scale.degrees[(row + key.scale.degrees.length - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch; }; + + + // Returns the first chord in the chord array that fits the given pitches. + this.getFirstFittingChordForPitches = function(pitches) + { + for (var i = 0; i < this.chords.length; i++) + { + var chord = this.chords[i]; + + if (pitches.length != chord.pitches.length) + continue; + + var offset = pitches[0]; + var match = true; + for (var j = 0; j < chord.pitches.length; j++) + { + if (chord.pitches[j] != pitches[j] - offset) + { + match = false; + break; + } + } + + if (match) + return chord; + } + + return null; + } } diff --git a/src/toolbox.js b/src/toolbox.js new file mode 100644 index 0000000..0f2222e --- /dev/null +++ b/src/toolbox.js @@ -0,0 +1,157 @@ +function Toolbox(div, editor) +{ + this.div = div; + + this.initCSS(); + + // Create elements. + this.mainLayout = document.createElement("table"); + this.mainLayout.style.margin = "auto"; + this.mainLayoutRow0 = document.createElement("tr"); this.mainLayout.appendChild(this.mainLayoutRow0); + this.mainLayoutCell00 = document.createElement("td"); this.mainLayoutRow0.appendChild(this.mainLayoutCell00); + this.mainLayoutCell01 = document.createElement("td"); this.mainLayoutRow0.appendChild(this.mainLayoutCell01); + this.div.appendChild(this.mainLayout); + + this.toolsLayout = document.createElement("table"); + this.toolsLayout.style.margin = "auto"; + this.toolsLayoutRow0 = document.createElement("tr"); this.toolsLayout.appendChild(this.toolsLayoutRow0); + this.toolsLayoutCell00 = document.createElement("td"); this.toolsLayoutRow0.appendChild(this.toolsLayoutCell00); + this.toolsLayoutRow1 = document.createElement("tr"); this.toolsLayout.appendChild(this.toolsLayoutRow1); + this.toolsLayoutCell10 = document.createElement("td"); this.toolsLayoutRow1.appendChild(this.toolsLayoutCell10); + this.mainLayoutCell01.appendChild(this.toolsLayout); + + this.buttonPlay = document.createElement("button"); + this.buttonPlay.innerHTML = "▶ Play"; + this.buttonPlay.className = "toolboxButton"; + this.buttonPlay.style.fontSize = "20px"; + this.mainLayoutCell00.appendChild(this.buttonPlay); + + this.buttonRewind = document.createElement("button"); + this.buttonRewind.innerHTML = "◀◀ Rewind"; + this.buttonRewind.className = "toolboxButton"; + this.mainLayoutCell00.appendChild(this.buttonRewind); + + this.labelKey = document.createElement("span"); + this.labelKey.className = "toolboxLabel"; + this.toolsLayoutCell00.appendChild(this.labelKey); + + this.toolsLayoutCell00.appendChild(document.createElement("br")); + + this.buttonAddKeyChange = document.createElement("button"); + this.buttonAddKeyChange.innerHTML = "Add Key Change"; + this.buttonAddKeyChange.className = "toolboxButton"; + this.toolsLayoutCell00.appendChild(this.buttonAddKeyChange); + + this.buttonAddMeterChange = document.createElement("button"); + this.buttonAddMeterChange.innerHTML = "Add Meter Change"; + this.buttonAddMeterChange.className = "toolboxButton"; + this.toolsLayoutCell00.appendChild(this.buttonAddMeterChange); + + this.toolsLayoutCell00.appendChild(document.createElement("br")); + + this.buttonNotes = []; + for (var i = 0; i < 12; i++) + { + this.buttonNotes[i] = document.createElement("button"); + this.buttonNotes[i].className = "toolboxNoteButton"; + this.toolsLayoutCell00.appendChild(this.buttonNotes[i]); + } + + this.toolsLayoutCell00.appendChild(document.createElement("br")); + + this.buttonChords = []; + for (var i = 0; i < 7; i++) + { + this.buttonChords[i] = document.createElement("button"); + this.buttonChords[i].className = "toolboxNoteChord"; + this.toolsLayoutCell00.appendChild(this.buttonChords[i]); + } + + // Set up callbacks. + this.editor = editor; + + var that = this; + editor.addOnCursorChanged(function() { that.editorOnCursorChanged(); }); + + this.editorOnCursorChanged(); +} + + +Toolbox.prototype.initCSS = function() +{ + var declarations = document.createTextNode( + ".toolboxButton { margin: 2px; border: 2px solid #aaaaaa; background: #ffffff; font-family: tahoma; font-size: 12px; outline: none; }" + + ".toolboxButton:hover { border-color: #cccccc; cursor: pointer; }" + + ".toolboxButton:active { border-color: #aaaaaa; background: #eeeeee; }" + + ".toolboxNoteButton { width: 20px; height: 30px; margin: 2px; padding: 0px; border: none; text-align: center; font-family: tahoma; font-size: 12px; outline: none; }" + + ".toolboxNoteButton:hover { cursor: pointer; border: 1px solid #ffffff; }" + + ".toolboxNoteButton:active { border: 2px solid #ffffff; }" + + ".toolboxNoteChord { width: 50px; height: 38px; margin: 2px; padding: 0px; border-style: solid; border-width: 3px 0 3px 0; background: #eeeeee; text-align: center; font-family: tahoma; font-size: 18px; outline: none; }" + + ".toolboxNoteChord:hover { cursor: pointer; background: #f8f8f8; }" + + ".toolboxNoteChord:active { background: #f0f0f0; }" + + ".toolboxLabel { font-family: tahoma; font-size: 14px; font-weight: bold; }"); + + var head = document.getElementsByTagName("head")[0]; + var styleElem = document.createElement("style"); + + styleElem.type = "text/css"; + + if (styleElem.styleSheet) + styleElem.styleSheet.cssText = declarations.nodeValue; + else + styleElem.appendChild(declarations); + + head.appendChild(styleElem); +} + + +Toolbox.prototype.editorOnCursorChanged = function() +{ + var key = this.editor.getKeyAtTick(this.editor.cursorTick); + + this.labelKey.innerHTML = "Key of " + theory.getNameForPitch(key.tonicPitch, key.scale) + " " + key.scale.name; + + for (var i = 0; i < 12; i++) + { + var pitch = (i + key.tonicPitch) % 12; + this.buttonNotes[i].innerHTML = theory.getNameForPitch(pitch, key.scale); + + var degree = theory.getDegreeForPitch(pitch, key); + + if ((degree % 1) == 0) + { + var color = theory.getColorForDegree(degree); + this.buttonNotes[i].style.background = color; + this.buttonNotes[i].style.color = "#000000"; + } + else + { + this.buttonNotes[i].style.background = "#dddddd"; + this.buttonNotes[i].style.color = "#888888"; + } + } + + + for (var i = 0; i < 7; i++) + { + var pitch1 = key.tonicPitch + key.scale.degrees[i]; + + var pitch2 = key.tonicPitch + key.scale.degrees[(i + 2) % key.scale.degrees.length]; + if ((i + 2) >= key.scale.degrees.length) + pitch2 += 12; + + var pitch3 = key.tonicPitch + key.scale.degrees[(i + 4) % key.scale.degrees.length]; + if ((i + 4) >= key.scale.degrees.length) + pitch3 += 12; + + var color = theory.getColorForDegree(i); + var chord = theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); + var numeral = theory.getRomanNumeralForPitch(pitch1, key); + + var romanText = chord.roman.replace("I", numeral).replace("i", numeral.toLowerCase()); + romanText += "" + chord.romanSup + ""; + + this.buttonChords[i].innerHTML = romanText; + this.buttonChords[i].style.borderColor = color; + } +} \ No newline at end of file diff --git a/test.html b/test.html index e3ccba8..19559a5 100644 --- a/test.html +++ b/test.html @@ -8,7 +8,9 @@ - +
+
+ @@ -18,5 +20,6 @@ + \ No newline at end of file diff --git a/test.js b/test.js index 94b7f41..1f00f5b 100644 --- a/test.js +++ b/test.js @@ -6,10 +6,14 @@ function testOnLoad() song.addKeyChange(new SongDataKeyChange(100, theory.scales[1], theory.D)); song.addMeterChange(new SongDataMeterChange(150, 3, 4)); song.addKeyChange(new SongDataKeyChange(200, theory.scales[2], theory.B)); - song.addKeyChange(new SongDataKeyChange(300, theory.scales[3], theory.E)); + song.addKeyChange(new SongDataKeyChange(300, theory.scales[4], theory.F)); + song.addChord(new SongDataChord(0, 100, theory.chords[0], theory.C)); + song.addChord(new SongDataChord(100, 50, theory.chords[1], theory.C)); + song.addChord(new SongDataChord(150, 50, theory.chords[2], theory.C)); + song.addChord(new SongDataChord(200, 150, theory.chords[3], theory.C)); song.addNote(new SongDataNote(0, 300, 12)); - song.addNote(new SongDataNote(0, 300, 5)); + song.addNote(new SongDataNote(0, 300, 2)); song.addNote(new SongDataNote(0, 300, 7)); song.addNote(new SongDataNote(325, 25, 0)); song.addNote(new SongDataNote(350, 25, 1)); @@ -27,4 +31,6 @@ function testOnLoad() var canvas = document.getElementById("editorCanvas"); var editor = new SongEditor(canvas, song); editor.refreshCanvas(); + + var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor); } \ No newline at end of file From ccaa742d2f2a67c0822165bec13e3c15a8a05ccd Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 28 Nov 2015 22:58:15 -0200 Subject: [PATCH 09/46] add delete key interaction --- src/editor.js | 3 ++- src/editor_interaction.js | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/editor.js b/src/editor.js index 2f7e5fc..9c06351 100644 --- a/src/editor.js +++ b/src/editor.js @@ -5,11 +5,12 @@ function SongEditor(canvas, songData) this.canvasWidth = parseFloat(canvas.width); this.canvasHeight = parseFloat(canvas.height); - // Set up mouse interaction. + // Set up mouse/keyboard interaction. var that = this; this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; + window.onkeydown = function(ev) { that.handleKeyDown(ev); }; // Set up callback arrays. this.cursorChangedCallbacks = []; diff --git a/src/editor_interaction.js b/src/editor_interaction.js index b7a63e4..bda20a8 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -567,4 +567,58 @@ SongEditor.prototype.handleMouseUp = function(ev) this.mouseDown = false; this.mouseDragAction = null; this.refreshCanvas(); +} + + +SongEditor.prototype.handleKeyDown = function(ev) +{ + var keyCode = (ev.key || ev.which || ev.keyCode); + + if (keyCode == 46) // Delete key + { + // Remove selected objects from the song data. + var selectedNotes = []; + for (var n = this.noteSelections.length - 1; n >= 0; n--) + { + if (this.noteSelections[n]) + { + selectedNotes.push(this.songData.notes[n]); + this.songData.notes.splice(n, 1); + } + } + + var selectedChords = []; + for (var n = this.chordSelections.length - 1; n >= 0; n--) + { + if (this.chordSelections[n]) + { + selectedChords.push(this.songData.chords[n]); + this.songData.chords.splice(n, 1); + } + } + + var selectedKeyChanges = []; + for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) + { + if (this.keyChangeSelections[n]) + { + selectedKeyChanges.push(this.songData.keyChanges[n]); + this.songData.keyChanges.splice(n, 1); + } + } + + var selectedMeterChanges = []; + for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) + { + if (this.meterChangeSelections[n]) + { + selectedMeterChanges.push(this.songData.meterChanges[n]); + this.songData.meterChanges.splice(n, 1); + } + } + + this.clearHover(); + this.refreshRepresentation(); + this.refreshCanvas(); + } } \ No newline at end of file From 76a92aa15808cc333e8e017efd5c5a8ae2b3c11c Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 29 Nov 2015 00:17:58 -0200 Subject: [PATCH 10/46] add toolbox chord categories --- src/editor_drawing.js | 3 +- src/editor_interaction.js | 3 +- src/theory.js | 11 +-- src/toolbox.js | 179 +++++++++++++++++++++++++++++++++----- test.html | 4 +- 5 files changed, 170 insertions(+), 30 deletions(-) diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 5e3e03f..e05eaab 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -350,7 +350,7 @@ SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hov // Draw roman symbol. var numeral = theory.getRomanNumeralForPitch(chord.rootPitch, block.key); - var romanText = chord.chord.roman.replace("I", numeral).replace("i", numeral.toLowerCase()); + var romanText = chord.chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); this.ctx.fillStyle = "#000000"; this.ctx.textAlign = "center"; @@ -378,6 +378,7 @@ SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hov this.ctx.font = "20px Tahoma"; this.ctx.fillText(chord.chord.romanSup, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, (pos.y1 + pos.y2) / 2 - 10, maxTextWidth - mainTextWidth - subTextWidth); + this.ctx.fillText(chord.chord.romanSub, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth + subTextWidth / 2, (pos.y1 + pos.y2) / 2 + 10, maxTextWidth - mainTextWidth - supTextWidth); // Draw highlights. this.ctx.save(); diff --git a/src/editor_interaction.js b/src/editor_interaction.js index bda20a8..c3c79c8 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -574,7 +574,7 @@ SongEditor.prototype.handleKeyDown = function(ev) { var keyCode = (ev.key || ev.which || ev.keyCode); - if (keyCode == 46) // Delete key + if (keyCode == 46 || keyCode == 8) // Delete/Backspace { // Remove selected objects from the song data. var selectedNotes = []; @@ -617,6 +617,7 @@ SongEditor.prototype.handleKeyDown = function(ev) } } + this.showCursor = true; this.clearHover(); this.refreshRepresentation(); this.refreshCanvas(); diff --git a/src/theory.js b/src/theory.js index a167ed6..f3d15d3 100644 --- a/src/theory.js +++ b/src/theory.js @@ -43,10 +43,11 @@ function Theory() this.chords = [ - { name: "Major", roman: "I", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, - { name: "Minor", roman: "i", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, - { name: "Diminished", roman: "i", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, - { name: "Augmented", roman: "I", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] } + { name: "Major", roman: "X", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, + { name: "Minor", roman: "x", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, + { name: "Diminished", roman: "x", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, + { name: "Augmented", roman: "X", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] }, + { name: "Suspended 2", roman: "X", romanSup: "", romanSub: "sus2", pitches: [ C, D, G ] } ]; @@ -61,7 +62,7 @@ function Theory() this.getRomanNumeralForPitch = function(pitch, key) { // TODO: Take the scale also into consideration for naming. - var numerals = ["I", "♯I", "II", "♯II", "III", "IV", "♯IV", "V", "♯V", "VI", "♯VI", "VII"]; + var numerals = ["I", "♭II", "II", "♭III", "III", "IV", "♭V", "V", "♭VI", "VI", "♭VII", "VII"]; return numerals[(pitch + 12 - key.tonicPitch) % 12]; }; diff --git a/src/toolbox.js b/src/toolbox.js index 0f2222e..ac1404f 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -47,6 +47,7 @@ function Toolbox(div, editor) this.buttonAddMeterChange.className = "toolboxButton"; this.toolsLayoutCell00.appendChild(this.buttonAddMeterChange); + this.toolsLayoutCell00.appendChild(document.createElement("br")); this.toolsLayoutCell00.appendChild(document.createElement("br")); this.buttonNotes = []; @@ -57,14 +58,44 @@ function Toolbox(div, editor) this.toolsLayoutCell00.appendChild(this.buttonNotes[i]); } + this.toolsLayoutCell00.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(document.createElement("br")); + + this.chordSelect = document.createElement("select"); + this.toolsLayoutCell00.appendChild(this.chordSelect); this.toolsLayoutCell00.appendChild(document.createElement("br")); + this.chordOptions = []; + for (var i = 0; i < theory.chords.length + 1; i++) + { + this.chordOptions[i] = document.createElement("option"); + this.chordOptions[i].value = i; + + var text; + if (i == 0) + { + text = "In Key"; + } + else + { + text = theory.chords[i - 1].roman.replace("X", "I").replace("x", "i"); + text += "" + theory.chords[i - 1].romanSup + ""; + text += "" + theory.chords[i - 1].romanSub + ""; + } + + this.chordOptions[i].innerHTML = text; + this.chordSelect.appendChild(this.chordOptions[i]); + } + this.buttonChords = []; - for (var i = 0; i < 7; i++) + for (var i = 0; i < 12; i++) { this.buttonChords[i] = document.createElement("button"); this.buttonChords[i].className = "toolboxNoteChord"; this.toolsLayoutCell00.appendChild(this.buttonChords[i]); + + if (i == 6) + this.toolsLayoutCell00.appendChild(document.createElement("br")); } // Set up callbacks. @@ -72,6 +103,7 @@ function Toolbox(div, editor) var that = this; editor.addOnCursorChanged(function() { that.editorOnCursorChanged(); }); + this.chordSelect.onclick = function() { that.editorOnCursorChanged(); }; this.editorOnCursorChanged(); } @@ -89,7 +121,9 @@ Toolbox.prototype.initCSS = function() ".toolboxNoteChord { width: 50px; height: 38px; margin: 2px; padding: 0px; border-style: solid; border-width: 3px 0 3px 0; background: #eeeeee; text-align: center; font-family: tahoma; font-size: 18px; outline: none; }" + ".toolboxNoteChord:hover { cursor: pointer; background: #f8f8f8; }" + ".toolboxNoteChord:active { background: #f0f0f0; }" + - ".toolboxLabel { font-family: tahoma; font-size: 14px; font-weight: bold; }"); + ".toolboxLabel { font-family: tahoma; font-size: 14px; font-weight: bold; }" + + ".toolboxText { font-family: tahoma; font-size: 12px; }" + + ".toolboxPalette { overflow-y: scroll; }"); var head = document.getElementsByTagName("head")[0]; var styleElem = document.createElement("style"); @@ -129,29 +163,132 @@ Toolbox.prototype.editorOnCursorChanged = function() this.buttonNotes[i].style.background = "#dddddd"; this.buttonNotes[i].style.color = "#888888"; } + + var that = this; + this.buttonNotes[i].onclick = function(pitch) + { + var thisPitch = pitch; + return function(ev) + { + ev.preventDefault(); + var note = new SongDataNote(that.editor.cursorTick, 25, thisPitch); + that.editor.songData.addNote(note); + that.editor.cursorTick += 25; + that.editor.showCursor = true; + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.editorOnCursorChanged(); + } + }(pitch); } - - for (var i = 0; i < 7; i++) - { - var pitch1 = key.tonicPitch + key.scale.degrees[i]; - - var pitch2 = key.tonicPitch + key.scale.degrees[(i + 2) % key.scale.degrees.length]; - if ((i + 2) >= key.scale.degrees.length) - pitch2 += 12; + var chordCategory = this.chordSelect.selectedIndex; - var pitch3 = key.tonicPitch + key.scale.degrees[(i + 4) % key.scale.degrees.length]; - if ((i + 4) >= key.scale.degrees.length) - pitch3 += 12; - - var color = theory.getColorForDegree(i); - var chord = theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); - var numeral = theory.getRomanNumeralForPitch(pitch1, key); + if (chordCategory == 0) + { + for (var i = 0; i < 7; i++) + { + var pitch1 = key.tonicPitch + key.scale.degrees[i]; + + var pitch2 = key.tonicPitch + key.scale.degrees[(i + 2) % key.scale.degrees.length]; + if ((i + 2) >= key.scale.degrees.length) + pitch2 += 12; + + var pitch3 = key.tonicPitch + key.scale.degrees[(i + 4) % key.scale.degrees.length]; + if ((i + 4) >= key.scale.degrees.length) + pitch3 += 12; + + var color = theory.getColorForDegree(i); + var chord = theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); + var numeral = theory.getRomanNumeralForPitch(pitch1, key); + + var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); + romanText += "" + chord.romanSup + ""; + romanText += "" + chord.romanSub + ""; + + this.buttonChords[i].innerHTML = romanText; + this.buttonChords[i].style.borderColor = color; + this.buttonChords[i].style.display = "visible"; + + var that = this; + this.buttonChords[i].onclick = function(chord, pitch) + { + var thisChord = chord; + var thisPitch = pitch; + return function(ev) + { + ev.preventDefault(); + var chord = new SongDataChord(that.editor.cursorTick, 50, thisChord, thisPitch); + that.editor.songData.addChord(chord); + that.editor.cursorTick += 50; + that.editor.showCursor = true; + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.editorOnCursorChanged(); + } + }(chord, pitch1); + } - var romanText = chord.roman.replace("I", numeral).replace("i", numeral.toLowerCase()); - romanText += "" + chord.romanSup + ""; + for (var i = 7; i < 12; i++) + this.buttonChords[i].style.display = "none"; + } + else + { + var inKeyIndex = 0; + var outOfKeyIndex = 7; - this.buttonChords[i].innerHTML = romanText; - this.buttonChords[i].style.borderColor = color; + for (var i = 0; i < 12; i++) + { + var rootPitch = (key.tonicPitch + i) % 12; + var degree = theory.getDegreeForPitch(rootPitch, key); + + var index = inKeyIndex; + if ((degree % 1) != 0) + index = outOfKeyIndex; + + var color = theory.getColorForDegree(degree); + var chord = theory.chords[chordCategory - 1]; + var numeral = theory.getRomanNumeralForPitch(rootPitch, key); + + var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); + romanText += "" + chord.romanSup + ""; + romanText += "" + chord.romanSub + ""; + + this.buttonChords[index].innerHTML = romanText; + this.buttonChords[index].style.display = "inline"; + if ((degree % 1) == 0) + { + this.buttonChords[index].style.borderColor = color; + this.buttonChords[index].style.color = "#000000"; + } + else + { + this.buttonChords[index].style.borderColor = "#dddddd"; + this.buttonChords[index].style.color = "#888888"; + } + + var that = this; + this.buttonChords[index].onclick = function(chord, pitch) + { + var thisChord = chord; + var thisPitch = pitch; + return function(ev) + { + ev.preventDefault(); + var chord = new SongDataChord(that.editor.cursorTick, 50, thisChord, thisPitch); + that.editor.songData.addChord(chord); + that.editor.cursorTick += 50; + that.editor.showCursor = true; + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.editorOnCursorChanged(); + } + }(chord, rootPitch); + + if ((degree % 1) != 0) + outOfKeyIndex++; + else + inKeyIndex++; + } } } \ No newline at end of file diff --git a/test.html b/test.html index 19559a5..cdb3488 100644 --- a/test.html +++ b/test.html @@ -8,9 +8,9 @@ -
-
+
+
From 0aa5f0fe819ae3ee378419e8d302c7779c3d174c Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 29 Nov 2015 01:33:04 -0200 Subject: [PATCH 11/46] add key/meter change editing --- src/editor.js | 15 +++ src/editor_interaction.js | 85 +++++++++++++ src/editor_representation.js | 2 + src/toolbox.js | 227 ++++++++++++++++++++++++++++++++--- 4 files changed, 309 insertions(+), 20 deletions(-) diff --git a/src/editor.js b/src/editor.js index 9c06351..55ad181 100644 --- a/src/editor.js +++ b/src/editor.js @@ -14,6 +14,7 @@ function SongEditor(canvas, songData) // Set up callback arrays. this.cursorChangedCallbacks = []; + this.selectionChangedCallbacks = []; // For each object in the song data, these arrays store a boolean // indicating whether the respective object is currently selected in the editor. @@ -21,6 +22,7 @@ function SongEditor(canvas, songData) this.chordSelections = []; this.keyChangeSelections = []; this.meterChangeSelections = []; + this.selectedObjects = 0; // The scaling from ticks to pixels. this.tickZoom = 1; @@ -91,8 +93,21 @@ SongEditor.prototype.addOnCursorChanged = function(func) } +SongEditor.prototype.addOnSelectionChanged = function(func) +{ + this.selectionChangedCallbacks.push(func); +} + + SongEditor.prototype.callOnCursorChanged = function() { for (var i = 0; i < this.cursorChangedCallbacks.length; i++) this.cursorChangedCallbacks[i](); +} + + +SongEditor.prototype.callOnSelectionChanged = function() +{ + for (var i = 0; i < this.selectionChangedCallbacks.length; i++) + this.selectionChangedCallbacks[i](); } \ No newline at end of file diff --git a/src/editor_interaction.js b/src/editor_interaction.js index c3c79c8..821c4f6 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -11,6 +11,65 @@ SongEditor.prototype.unselectAll = function() for (var i = 0; i < this.meterChangeSelections.length; i++) this.meterChangeSelections[i] = false; + + this.selectedObjects = 0; + this.callOnSelectionChanged(); +} + + +SongEditor.prototype.selectKeyChange = function(keyChange) +{ + for (var n = 0; n < this.songData.keyChanges.length; n++) + { + if (this.songData.keyChanges[n] == keyChange) + { + this.selectedObjects++; + this.keyChangeSelections[n] = true; + } + } +} + + +SongEditor.prototype.selectMeterChange = function(meterChange) +{ + for (var n = 0; n < this.songData.meterChanges.length; n++) + { + if (this.songData.meterChanges[n] == meterChange) + { + this.selectedObjects++; + this.meterChangeSelections[n] = true; + } + } +} + + +SongEditor.prototype.getUniqueKeyChangeSelected = function() +{ + if (this.selectedObjects != 1) + return null; + + for (var i = 0; i < this.keyChangeSelections.length; i++) + { + if (this.keyChangeSelections[i]) + return this.songData.keyChanges[i]; + } + + return null; +} + + +SongEditor.prototype.getUniqueMeterChangeSelected = function() +{ + if (this.selectedObjects != 1) + return null; + + for (var i = 0; i < this.meterChangeSelections.length; i++) + { + if (this.meterChangeSelections[i]) + return this.songData.meterChanges[i]; + } + + return null; } @@ -380,6 +439,9 @@ SongEditor.prototype.handleMouseDown = function(ev) if (this.hoverNote >= 0) { this.showCursor = false; + if (!this.noteSelections[this.hoverNote]) + this.selectedObjects++; + this.noteSelections[this.hoverNote] = true; if (this.hoverStretchR) @@ -400,6 +462,9 @@ SongEditor.prototype.handleMouseDown = function(ev) else if (this.hoverChord >= 0) { this.showCursor = false; + if (!this.chordSelections[this.hoverChord]) + this.selectedObjects++; + this.chordSelections[this.hoverChord] = true; if (this.hoverStretchR) @@ -419,18 +484,25 @@ SongEditor.prototype.handleMouseDown = function(ev) } else if (this.hoverKeyChange >= 0) { + if (!this.keyChangeSelections[this.hoverKeyChange]) + this.selectedObjects++; + this.showCursor = false; this.keyChangeSelections[this.hoverKeyChange] = true; this.mouseDragAction = "move"; } else if (this.hoverMeterChange >= 0) { + if (!this.meterChangeSelections[this.hoverMeterChange]) + this.selectedObjects++; + this.showCursor = false; this.meterChangeSelections[this.hoverMeterChange] = true; this.mouseDragAction = "move"; } this.mouseDown = true; + this.callOnSelectionChanged(); this.refreshCanvas(); this.callOnCursorChanged(); } @@ -532,7 +604,10 @@ SongEditor.prototype.handleMouseUp = function(ev) for (var m = 0; m < newNotes.length; m++) { if (this.songData.notes[n] == newNotes[m]) + { + this.selectedObjects++; this.noteSelections[n] = true; + } } } @@ -541,7 +616,10 @@ SongEditor.prototype.handleMouseUp = function(ev) for (var m = 0; m < newChords.length; m++) { if (this.songData.chords[n] == newChords[m]) + { + this.selectedObjects++; this.chordSelections[n] = true; + } } } @@ -550,7 +628,10 @@ SongEditor.prototype.handleMouseUp = function(ev) for (var m = 0; m < newKeyChanges.length; m++) { if (this.songData.keyChanges[n] == newKeyChanges[m]) + { + this.selectedObjects++; this.keyChangeSelections[n] = true; + } } } @@ -559,7 +640,10 @@ SongEditor.prototype.handleMouseUp = function(ev) for (var m = 0; m < newMeterChanges.length; m++) { if (this.songData.meterChanges[n] == newMeterChanges[m]) + { + this.selectedObjects++; this.meterChangeSelections[n] = true; + } } } } @@ -621,5 +705,6 @@ SongEditor.prototype.handleKeyDown = function(ev) this.clearHover(); this.refreshRepresentation(); this.refreshCanvas(); + this.callOnSelectionChanged(); } } \ No newline at end of file diff --git a/src/editor_representation.js b/src/editor_representation.js index db8c830..01dc667 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -11,6 +11,8 @@ SongEditor.prototype.refreshRepresentation = function() // Clear selection arrays and push a boolean false for each object in the song data, // effectively unselecting everything, while also accomodating added/removed objects. + this.selectedObjects = 0; + this.noteSelections = []; for (var i = 0; i < this.songData.notes.length; i++) this.noteSelections.push(false); diff --git a/src/toolbox.js b/src/toolbox.js index ac1404f..205e1ce 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -4,6 +4,7 @@ function Toolbox(div, editor) this.initCSS(); + // Create elements. this.mainLayout = document.createElement("table"); this.mainLayout.style.margin = "auto"; @@ -20,6 +21,8 @@ function Toolbox(div, editor) this.toolsLayoutCell10 = document.createElement("td"); this.toolsLayoutRow1.appendChild(this.toolsLayoutCell10); this.mainLayoutCell01.appendChild(this.toolsLayout); + + // Playback controls. this.buttonPlay = document.createElement("button"); this.buttonPlay.innerHTML = "▶ Play"; this.buttonPlay.className = "toolboxButton"; @@ -31,45 +34,59 @@ function Toolbox(div, editor) this.buttonRewind.className = "toolboxButton"; this.mainLayoutCell00.appendChild(this.buttonRewind); + + // Editing controls. this.labelKey = document.createElement("span"); this.labelKey.className = "toolboxLabel"; + this.labelKey.appendChild(document.createElement("br")); this.toolsLayoutCell00.appendChild(this.labelKey); - this.toolsLayoutCell00.appendChild(document.createElement("br")); + + // Add Key/Meter Changes buttons. + this.changesSpan = document.createElement("span"); this.buttonAddKeyChange = document.createElement("button"); this.buttonAddKeyChange.innerHTML = "Add Key Change"; this.buttonAddKeyChange.className = "toolboxButton"; - this.toolsLayoutCell00.appendChild(this.buttonAddKeyChange); + this.changesSpan.appendChild(this.buttonAddKeyChange); this.buttonAddMeterChange = document.createElement("button"); this.buttonAddMeterChange.innerHTML = "Add Meter Change"; this.buttonAddMeterChange.className = "toolboxButton"; - this.toolsLayoutCell00.appendChild(this.buttonAddMeterChange); + this.changesSpan.appendChild(this.buttonAddMeterChange); + + this.changesSpan.appendChild(document.createElement("br")); + this.changesSpan.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(this.changesSpan); - this.toolsLayoutCell00.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(document.createElement("br")); + + // Note buttons. + this.notesSpan = document.createElement("span"); this.buttonNotes = []; for (var i = 0; i < 12; i++) { this.buttonNotes[i] = document.createElement("button"); this.buttonNotes[i].className = "toolboxNoteButton"; - this.toolsLayoutCell00.appendChild(this.buttonNotes[i]); + this.notesSpan.appendChild(this.buttonNotes[i]); } - this.toolsLayoutCell00.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(document.createElement("br")); + this.notesSpan.appendChild(document.createElement("br")); + this.notesSpan.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(this.notesSpan); + + + // Chord buttons. + this.chordsSpan = document.createElement("span"); this.chordSelect = document.createElement("select"); - this.toolsLayoutCell00.appendChild(this.chordSelect); - this.toolsLayoutCell00.appendChild(document.createElement("br")); + this.chordsSpan.appendChild(this.chordSelect); + this.chordsSpan.appendChild(document.createElement("br")); this.chordOptions = []; for (var i = 0; i < theory.chords.length + 1; i++) { this.chordOptions[i] = document.createElement("option"); - this.chordOptions[i].value = i; var text; if (i == 0) @@ -92,20 +109,104 @@ function Toolbox(div, editor) { this.buttonChords[i] = document.createElement("button"); this.buttonChords[i].className = "toolboxNoteChord"; - this.toolsLayoutCell00.appendChild(this.buttonChords[i]); + this.chordsSpan.appendChild(this.buttonChords[i]); if (i == 6) - this.toolsLayoutCell00.appendChild(document.createElement("br")); + this.chordsSpan.appendChild(document.createElement("br")); + } + + this.toolsLayoutCell00.appendChild(this.chordsSpan); + + + // Key Change settings. + this.keyChangeSpan = document.createElement("span"); + + var keyChangeEditingLabelSpan = document.createElement("span"); + keyChangeEditingLabelSpan.innerHTML = "Editing Key Change"; + keyChangeEditingLabelSpan.className = "toolboxLabel"; + this.keyChangeSpan.appendChild(keyChangeEditingLabelSpan); + this.keyChangeSpan.appendChild(document.createElement("br")); + + this.keyChangeTonicSelect = document.createElement("select"); + this.keyChangeTonicOptions = []; + for (var i = 0; i < 12; i++) + { + this.keyChangeTonicOptions[i] = document.createElement("option"); + this.keyChangeTonicSelect.appendChild(this.keyChangeTonicOptions[i]); } + this.keyChangeSpan.appendChild(this.keyChangeTonicSelect); + + var keyChangeDividerSpan = document.createElement("span"); + keyChangeDividerSpan.innerHTML = " "; + keyChangeDividerSpan.className = "toolboxText"; + this.keyChangeSpan.appendChild(keyChangeDividerSpan); + + this.keyChangeScaleSelect = document.createElement("select"); + this.keyChangeScaleOptions = []; + for (var i = 0; i < theory.scales.length; i++) + { + this.keyChangeScaleOptions[i] = document.createElement("option"); + this.keyChangeScaleOptions[i].innerHTML = theory.scales[i].name; + this.keyChangeScaleSelect.appendChild(this.keyChangeScaleOptions[i]); + } + this.keyChangeSpan.appendChild(this.keyChangeScaleSelect); + + this.keyChangeSpan.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(this.keyChangeSpan); + + + // Meter Change settings. + this.meterChangeSpan = document.createElement("span"); + + var meterChangeEditingLabelSpan = document.createElement("span"); + meterChangeEditingLabelSpan.innerHTML = "Editing Meter Change"; + meterChangeEditingLabelSpan.className = "toolboxLabel"; + this.meterChangeSpan.appendChild(meterChangeEditingLabelSpan); + this.meterChangeSpan.appendChild(document.createElement("br")); + + this.meterChangeNumeratorSelect = document.createElement("select"); + this.meterChangeNumeratorOptions = []; + for (var i = 0; i < 20; i++) + { + this.meterChangeNumeratorOptions[i] = document.createElement("option"); + this.meterChangeNumeratorOptions[i].innerHTML = "" + (i + 1); + this.meterChangeNumeratorSelect.appendChild(this.meterChangeNumeratorOptions[i]); + } + this.meterChangeSpan.appendChild(this.meterChangeNumeratorSelect); + + var meterChangeDividerSpan = document.createElement("span"); + meterChangeDividerSpan.innerHTML = " / "; + meterChangeDividerSpan.className = "toolboxText"; + this.meterChangeSpan.appendChild(meterChangeDividerSpan); + + this.meterChangeDenominatorSelect = document.createElement("select"); + this.meterChangeDenominatorOptions = []; + this.meterChangeDenominators = [ 2, 4, 8, 16 ]; + for (var i = 0; i < this.meterChangeDenominators.length; i++) + { + this.meterChangeDenominatorOptions[i] = document.createElement("option"); + this.meterChangeDenominatorOptions[i].innerHTML = "" + this.meterChangeDenominators[i]; + this.meterChangeDenominatorSelect.appendChild(this.meterChangeDenominatorOptions[i]); + } + this.meterChangeSpan.appendChild(this.meterChangeDenominatorSelect); + + this.meterChangeSpan.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(this.meterChangeSpan); + // Set up callbacks. this.editor = editor; var that = this; - editor.addOnCursorChanged(function() { that.editorOnCursorChanged(); }); - this.chordSelect.onclick = function() { that.editorOnCursorChanged(); }; + editor.addOnCursorChanged(function() { that.refresh(); }); + editor.addOnSelectionChanged(function() { that.refreshSelection(); }); + this.chordSelect.onchange = function() { that.refresh(); }; + this.keyChangeTonicSelect.onchange = function() { that.editKeyChange(); }; + this.keyChangeScaleSelect.onchange = function() { that.editKeyChange(); }; + this.meterChangeNumeratorSelect.onchange = function() { that.editMeterChange(); }; + this.meterChangeDenominatorSelect.onchange = function() { that.editMeterChange(); }; - this.editorOnCursorChanged(); + this.refresh(); } @@ -139,11 +240,12 @@ Toolbox.prototype.initCSS = function() } -Toolbox.prototype.editorOnCursorChanged = function() +Toolbox.prototype.refresh = function() { var key = this.editor.getKeyAtTick(this.editor.cursorTick); this.labelKey.innerHTML = "Key of " + theory.getNameForPitch(key.tonicPitch, key.scale) + " " + key.scale.name; + this.labelKey.appendChild(document.createElement("br")); for (var i = 0; i < 12; i++) { @@ -177,7 +279,7 @@ Toolbox.prototype.editorOnCursorChanged = function() that.editor.showCursor = true; that.editor.refreshRepresentation(); that.editor.refreshCanvas(); - that.editorOnCursorChanged(); + that.refresh(); } }(pitch); } @@ -224,7 +326,7 @@ Toolbox.prototype.editorOnCursorChanged = function() that.editor.showCursor = true; that.editor.refreshRepresentation(); that.editor.refreshCanvas(); - that.editorOnCursorChanged(); + that.refresh(); } }(chord, pitch1); } @@ -281,7 +383,7 @@ Toolbox.prototype.editorOnCursorChanged = function() that.editor.showCursor = true; that.editor.refreshRepresentation(); that.editor.refreshCanvas(); - that.editorOnCursorChanged(); + that.refresh(); } }(chord, rootPitch); @@ -291,4 +393,89 @@ Toolbox.prototype.editorOnCursorChanged = function() inKeyIndex++; } } +} + + +Toolbox.prototype.refreshSelection = function() +{ + this.labelKey.style.display = "none"; + this.changesSpan.style.display = "none"; + this.notesSpan.style.display = "none"; + this.chordsSpan.style.display = "none"; + this.keyChangeSpan.style.display = "none"; + this.meterChangeSpan.style.display = "none"; + + + var keyChange = this.editor.getUniqueKeyChangeSelected(); + if (keyChange != null) + { + var scaleIndex = 0; + for (var i = 0; i < theory.scales.length; i++) + { + if (keyChange.scale == theory.scales[i]) + { + scaleIndex = i; + break; + } + } + + this.keyChangeScaleSelect.selectedIndex = scaleIndex; + + for (var i = 0; i < 12; i++) + { + var pitch = i; + this.keyChangeTonicOptions[i].innerHTML = theory.getNameForPitch(pitch, keyChange); + } + + this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; + + this.keyChangeSpan.style.display = "inline"; + return; + } + + + var meterChange = this.editor.getUniqueMeterChangeSelected(); + if (meterChange != null) + { + this.meterChangeNumeratorSelect.selectedIndex = meterChange.numerator - 1; + if (meterChange.denominator == 2) this.meterChangeDenominatorSelect.selectedIndex = 0; + else if (meterChange.denominator == 4) this.meterChangeDenominatorSelect.selectedIndex = 1; + else if (meterChange.denominator == 8) this.meterChangeDenominatorSelect.selectedIndex = 2; + else if (meterChange.denominator == 16) this.meterChangeDenominatorSelect.selectedIndex = 3; + + this.meterChangeSpan.style.display = "inline"; + return; + } + + + this.labelKey.style.display = "inline"; + this.changesSpan.style.display = "inline"; + this.notesSpan.style.display = "inline"; + this.chordsSpan.style.display = "inline"; +} + + +Toolbox.prototype.editKeyChange = function() +{ + var keyChange = this.editor.getUniqueKeyChangeSelected(); + keyChange.scale = theory.scales[this.keyChangeScaleSelect.selectedIndex]; + keyChange.tonicPitch = this.keyChangeTonicSelect.selectedIndex; + + this.editor.refreshRepresentation(); + this.editor.selectKeyChange(keyChange); + this.editor.refreshCanvas(); + this.refresh(); +} + + +Toolbox.prototype.editMeterChange = function() +{ + var meterChange = this.editor.getUniqueMeterChangeSelected(); + meterChange.numerator = this.meterChangeNumeratorSelect.selectedIndex + 1; + meterChange.denominator = this.meterChangeDenominators[this.meterChangeDenominatorSelect.selectedIndex]; + + this.editor.refreshRepresentation(); + this.editor.selectMeterChange(meterChange); + this.editor.refreshCanvas(); + this.refresh(); } \ No newline at end of file From 79b61c2a66d13ab2d72606f7c431b9aa10b01faa Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 29 Nov 2015 19:13:23 -0200 Subject: [PATCH 12/46] add synth; add playback --- src/editor.js | 18 +- src/editor_drawing.js | 10 +- src/editor_interaction.js | 16 ++ src/editor_representation.js | 6 + src/songdata.js | 4 +- src/synth.js | 116 ++++++++++++ src/theory.js | 82 +++++++- src/toolbox.js | 357 +++++++++++++++++++++++++---------- test.html | 1 + test.js | 61 +++--- 10 files changed, 531 insertions(+), 140 deletions(-) create mode 100644 src/synth.js diff --git a/src/editor.js b/src/editor.js index 55ad181..1d4f546 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1,4 +1,4 @@ -function SongEditor(canvas, songData) +function SongEditor(canvas, songData, synth) { this.canvas = canvas; this.ctx = canvas.getContext("2d"); @@ -25,10 +25,10 @@ function SongEditor(canvas, songData) this.selectedObjects = 0; // The scaling from ticks to pixels. - this.tickZoom = 1; + this.tickZoom = 0.1; // The tick grid which the cursor is snapped to. - this.tickSnap = 5; + this.tickSnap = 960 / 16; // These arrays store representation objects, which tell things like // the object positioning/layout, and where they can be interacted with the mouse. @@ -43,6 +43,7 @@ function SongEditor(canvas, songData) this.showCursor = true; // These control mouse interaction. + this.interactionEnabled = true; this.mouseDown = false; this.mouseDragAction = null; this.mouseDragCurrent = { x: 0, y: 0 }; @@ -59,7 +60,7 @@ function SongEditor(canvas, songData) this.hoverStretchL = false; // Layout constants. - this.WHOLE_NOTE_DURATION = 100; + this.WHOLE_NOTE_DURATION = 960; this.MARGIN_LEFT = 4; this.MARGIN_RIGHT = 4; this.MARGIN_TOP = 4; @@ -75,8 +76,9 @@ function SongEditor(canvas, songData) this.KEYCHANGE_BAR_WIDTH = 20; this.METERCHANGE_BAR_WIDTH = 4; - // Finally, set the song data. + // Finally, set the song data, and the synth manager. this.setData(songData); + this.synth = synth; } @@ -87,6 +89,12 @@ SongEditor.prototype.setData = function(songData) } +SongEditor.prototype.setInteractionEnabled = function(enable) +{ + this.interactionEnabled = enable; +} + + SongEditor.prototype.addOnCursorChanged = function(func) { this.cursorChangedCallbacks.push(func); diff --git a/src/editor_drawing.js b/src/editor_drawing.js index e05eaab..edc21ec 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -133,7 +133,6 @@ SongEditor.prototype.refreshCanvas = function() for (var i = 0; i < this.viewKeyChanges.length; i++) { var keyChange = this.viewKeyChanges[i]; - var textX = keyChange.x2 + 8; if (this.keyChangeSelections[i]) { @@ -181,13 +180,15 @@ SongEditor.prototype.refreshCanvas = function() } // Draw key name. + var textX = keyChange.x2 + 8; this.ctx.font = "14px Tahoma"; var songKeyChange = this.songData.keyChanges[keyChange.keyChangeIndex]; this.ctx.fillStyle = KEY_BORDER_COLOR; this.ctx.fillText( "" + theory.getNameForPitch(songKeyChange.tonicPitch, songKeyChange.scale) + " " + songKeyChange.scale.name, textX, - keyChange.y1); + keyChange.y1, + this.viewBlocks[keyChange.blockIndex].x2 - keyChange.x1 - 32); // Draw pitches. this.ctx.font = "10px Tahoma"; @@ -213,7 +214,6 @@ SongEditor.prototype.refreshCanvas = function() for (var i = 0; i < this.viewMeterChanges.length; i++) { var meterChange = this.viewMeterChanges[i]; - var textX = meterChange.x2 + 8; if (this.meterChangeSelections[i]) { @@ -261,12 +261,14 @@ SongEditor.prototype.refreshCanvas = function() } // Draw numbers. + var textX = meterChange.x2 + 8; var songMeterChange = this.songData.meterChanges[meterChange.meterChangeIndex]; this.ctx.fillStyle = METER_BORDER_COLOR; this.ctx.fillText( "" + songMeterChange.numerator + " / " + songMeterChange.denominator, textX, - meterChange.y1); + meterChange.y1, + this.viewBlocks[meterChange.blockIndex].x2 - meterChange.x1 - 16); } diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 821c4f6..6ae1f7b 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -313,6 +313,9 @@ SongEditor.prototype.handleMouseMove = function(ev) { ev.preventDefault(); var mousePos = transformMousePosition(this.canvas, ev); + + if (!this.interactionEnabled) + return; this.canvas.style.cursor = "default"; this.mouseDragCurrent = mousePos; @@ -426,6 +429,9 @@ SongEditor.prototype.handleMouseDown = function(ev) ev.preventDefault(); var mousePos = transformMousePosition(this.canvas, ev); + if (!this.interactionEnabled) + return; + if (!ev.ctrlKey) this.unselectAll(); @@ -443,6 +449,8 @@ SongEditor.prototype.handleMouseDown = function(ev) this.selectedObjects++; this.noteSelections[this.hoverNote] = true; + this.cursorTick = this.songData.notes[this.hoverNote].tick; + theory.playNoteSample(this.synth, this.songData.notes[this.hoverNote].pitch); if (this.hoverStretchR) { @@ -466,6 +474,8 @@ SongEditor.prototype.handleMouseDown = function(ev) this.selectedObjects++; this.chordSelections[this.hoverChord] = true; + this.cursorTick = this.songData.chords[this.hoverChord].tick; + theory.playChordSample(this.synth, this.songData.chords[this.hoverChord].chord, this.songData.chords[this.hoverChord].rootPitch); if (this.hoverStretchR) { @@ -513,6 +523,9 @@ SongEditor.prototype.handleMouseUp = function(ev) ev.preventDefault(); var mousePos = transformMousePosition(this.canvas, ev); + if (!this.interactionEnabled) + return; + // Apply dragged modifications. if (this.mouseDown && this.mouseDragAction != null) { @@ -656,6 +669,9 @@ SongEditor.prototype.handleMouseUp = function(ev) SongEditor.prototype.handleKeyDown = function(ev) { + if (!this.interactionEnabled) + return; + var keyCode = (ev.key || ev.which || ev.keyCode); if (keyCode == 46 || keyCode == 8) // Delete/Backspace diff --git a/src/editor_representation.js b/src/editor_representation.js index 01dc667..0955416 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -210,10 +210,16 @@ SongEditor.prototype.refreshRepresentation = function() // Apply key/meter changes to the next block. if (nextIsWhat == NEXT_IS_KEYCHANGE) + { + this.viewKeyChanges[this.viewKeyChanges.length - 1].blockIndex = curBlock; this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; + } else if (nextIsWhat == NEXT_IS_METERCHANGE) + { + this.viewMeterChanges[this.viewMeterChanges.length - 1].blockIndex = curBlock; this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; + } } } diff --git a/src/songdata.js b/src/songdata.js index 352a1ce..49c0419 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -1,8 +1,8 @@ function SongData() { this.beatsPerMinute = 120; - this.ticksPerBeat = 120; - this.lastTick = 1000; + this.ticksPerBeat = 960; + this.lastTick = 9600; this.notes = []; this.chords = []; this.keyChanges = []; diff --git a/src/synth.js b/src/synth.js new file mode 100644 index 0000000..9c288c7 --- /dev/null +++ b/src/synth.js @@ -0,0 +1,116 @@ +function Synth() +{ + this.audioCtx = new AudioContext(); + this.globalVolume = 0.1; + this.voices = []; + this.delayedNotes = []; +} + + +Synth.prototype.process = function(deltaTime) +{ + for (var i = this.delayedNotes.length - 1; i >= 0; i--) + { + var note = this.delayedNotes[i]; + note.delay -= deltaTime; + + if (note.delay <= 0) + { + this.playNote(note.midiPitch, note.duration, note.volume); + this.delayedNotes.splice(i, 1); + } + } + + for (var i = this.voices.length - 1; i >= 0; i--) + { + var voice = this.voices[i]; + + voice.time += deltaTime; + voice.duration -= deltaTime; + + if (voice.time >= voice.duration) + { + this.stopVoice(i); + this.voices.splice(i, 1); + } + else + { + for (var j = 0; j < voice.gainNodes.length; j++) + { + voice.gainNodes[j].gain.value = voice.amplitudes[j] * voice.volume * (1 - voice.time / voice.duration) * this.globalVolume; + } + } + } +} + + +Synth.prototype.playNote = function(midiPitch, duration, volume) +{ + var pitchInHertz = Math.pow(2, (midiPitch - 69) / 12) * 440; + + var voice = { }; + voice.oscillators = []; + voice.gainNodes = []; + voice.volume = volume; + + voice.amplitudes = [ + 1.0, 0.399064778, 0.229404484, 0.151836061, + 0.196754229, 0.093742264, 0.060871957, + 0.138605419, 0.010535002, 0.071021868, + 0.029954614, 0.051299684, 0.055948288, + 0.066208224, 0.010067391, 0.00753679, + 0.008196947, 0.012955577, 0.007316738, + 0.006216476, 0.005116215, 0.006243983, + 0.002860679, 0.002558108, 0.0, 0.001650392]; + + for (var i = 1; i <= voice.amplitudes.length; i++) + { + var oscillator = this.audioCtx.createOscillator(); + oscillator.frequency.value = pitchInHertz * i; + oscillator.type = "sine"; + + var gainNode = this.audioCtx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(this.audioCtx.destination); + gainNode.gain.value = voice.amplitudes[i - 1] * volume * this.globalVolume; + + oscillator.start(0); + + voice.oscillators.push(oscillator); + voice.gainNodes.push(gainNode); + } + + voice.time = 0; + voice.duration = duration; + this.voices.push(voice); +} + + +Synth.prototype.playNoteDelayed = function(midiPitch, delay, duration, volume) +{ + this.delayedNotes.push({ + midiPitch: midiPitch, + delay: delay, + duration: duration, + volume: volume + }); +} + + +Synth.prototype.stopAll = function() +{ + for (var i = this.voices.length - 1; i >= 0; i--) + { + this.stopVoice(i); + } + + this.voices = []; +} + + +Synth.prototype.stopVoice = function(index) +{ + var voice = this.voices[index]; + for (var i = 0; i < voice.oscillators.length; i++) + voice.oscillators[i].stop(0); +} \ No newline at end of file diff --git a/src/theory.js b/src/theory.js index f3d15d3..7199021 100644 --- a/src/theory.js +++ b/src/theory.js @@ -34,20 +34,54 @@ function Theory() this.scales = [ { name: "Major", degrees: [ C, D, E, F, G, A, B ] }, - { name: "Dorian", degrees: [ C, D, Ds, F, G, A, As ] }, - { name: "Mixolydian", degrees: [ C, D, E, F, G, A, As ] }, - { name: "Natural Minor", degrees: [ C, D, Ds, F, G, Gs, As ] }, - { name: "Phrygian Dominant", degrees: [ C, Cs, E, F, G, Gs, As ] } + { name: "Dorian", degrees: null }, + { name: "Phrygian", degrees: null }, + { name: "Lydian", degrees: null }, + { name: "Mixolydian", degrees: null }, + { name: "Natural Minor", degrees: null }, + { name: "Locrian", degrees: null }, + { name: "Harmonic Minor", degrees: [ C, D, Ds, F, G, Gs, B ] }, + { name: "Double Harmonic", degrees: [ C, Cs, E, F, G, Gs, B ] }, + { name: "Lydian ♯2 ♯6", degrees: null }, + { name: "Ultraphrygian", degrees: null }, + { name: "Hungarian Minor", degrees: null }, + { name: "Oriental", degrees: null }, + { name: "Ionian Augmented ♯2", degrees: null }, + { name: "Locrian ♭♭3 ♭♭7", degrees: null }, + { name: "Phrygian Dominant", degrees: [ C, Cs, E, F, G, Gs, As ] }, ]; + // Generate the other modes of the base scales. + for (var i = 0; i < this.scales.length; i++) + { + if (this.scales[i].degrees == null) + { + this.scales[i].degrees = []; + var offset = this.scales[i - 1].degrees[1]; + for (var j = 0; j < 7; j++) + { + this.scales[i].degrees[j] = (this.scales[i - 1].degrees[(j + 1) % 7] + 12 - offset) % 12; + } + } + } + this.chords = [ - { name: "Major", roman: "X", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, - { name: "Minor", roman: "x", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, - { name: "Diminished", roman: "x", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, - { name: "Augmented", roman: "X", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] }, - { name: "Suspended 2", roman: "X", romanSup: "", romanSub: "sus2", pitches: [ C, D, G ] } + { name: "Major", roman: "X", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, + { name: "Minor", roman: "x", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, + { name: "Diminished", roman: "x", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, + { name: "Augmented", roman: "X", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] }, + { name: "Flat Fifth(?)", roman: "X", romanSup: "(♭5)", romanSub: "", pitches: [ C, E, Fs ] }, + { name: "?", roman: "X", romanSup: "?", romanSub: "", pitches: [ C, D, Fs ] }, + { name: "Dominant Seventh", roman: "X", romanSup: "7", romanSub: "", pitches: [ C, E, G, As ] }, + { name: "Major Seventh", roman: "X", romanSup: "M7", romanSub: "", pitches: [ C, E, G, B ] }, + { name: "Minor Seventh", roman: "x", romanSup: "7", romanSub: "", pitches: [ C, Ds, G, As ] }, + { name: "Minor-Major Seventh", roman: "X", romanSup: "m(M7)", romanSub: "", pitches: [ C, Ds, G, B ] }, + { name: "Diminished Seventh", roman: "x", romanSup: "o7", romanSub: "", pitches: [ C, Ds, G, A ] }, + { name: "Half-Diminished Seventh", roman: "x", romanSup: "ø7", romanSub: "", pitches: [ C, Ds, Fs, As ] }, + { name: "Augmented Seventh", roman: "X", romanSup: "+7", romanSub: "", pitches: [ C, E, Gs, As ] }, + { name: "Augmented Major Seventh", roman: "X", romanSup: "+(M7)", romanSub: "", pitches: [ C, E, Gs, B ] } ]; @@ -185,6 +219,36 @@ function Theory() return chord; } + console.log("Missing chord data for pitches:"); + console.log(pitches); return null; } + + + // Plays a sample of the given note. + this.playNoteSample = function(synth, pitch) + { + synth.playNote(pitch + 60, 960 / 8, 1); + } + + + // Plays a sample of the given chord. + this.playChordSample = function(synth, chord, rootPitch) + { + for (var k = 0; k < chord.pitches.length; k++) + { + synth.playNote((rootPitch + chord.pitches[k]) % 12 + 60, 960 / 8, 0.75); + } + } + + + // Plays a sample of the given scale. + this.playScaleSample = function(synth, scale, tonicPitch) + { + for (var k = 0; k < scale.degrees.length; k++) + { + synth.playNoteDelayed((tonicPitch + scale.degrees[k]) + 60, k * 5, 960 / 8, 1); + } + synth.playNoteDelayed((tonicPitch + scale.degrees[0] + 12) + 60, scale.degrees.length * 5, 960 / 8, 1); + } } diff --git a/src/toolbox.js b/src/toolbox.js index 205e1ce..5173688 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -1,6 +1,9 @@ -function Toolbox(div, editor) +function Toolbox(div, editor, synth) { this.div = div; + this.playing = false; + this.playTimer = null; + this.initCSS(); @@ -35,6 +38,91 @@ function Toolbox(div, editor) this.mainLayoutCell00.appendChild(this.buttonRewind); + // Key Change settings. + this.keyChangeSpan = document.createElement("span"); + this.keyChangeSpan.style.background = "#cccccc"; + + var keyChangeEditingLabelSpan = document.createElement("span"); + keyChangeEditingLabelSpan.innerHTML = "Edit Key Change"; + keyChangeEditingLabelSpan.className = "toolboxLabel"; + this.keyChangeSpan.appendChild(keyChangeEditingLabelSpan); + this.keyChangeSpan.appendChild(document.createElement("br")); + + this.keyChangeTonicSelect = document.createElement("select"); + this.keyChangeTonicOptions = []; + for (var i = 0; i < 12; i++) + { + this.keyChangeTonicOptions[i] = document.createElement("option"); + this.keyChangeTonicSelect.appendChild(this.keyChangeTonicOptions[i]); + } + this.keyChangeSpan.appendChild(this.keyChangeTonicSelect); + + var keyChangeDividerSpan = document.createElement("span"); + keyChangeDividerSpan.innerHTML = " "; + keyChangeDividerSpan.className = "toolboxText"; + this.keyChangeSpan.appendChild(keyChangeDividerSpan); + + this.keyChangeScaleSelect = document.createElement("select"); + this.keyChangeScaleOptions = []; + for (var i = 0; i < theory.scales.length; i++) + { + this.keyChangeScaleOptions[i] = document.createElement("option"); + this.keyChangeScaleOptions[i].innerHTML = theory.scales[i].name; + this.keyChangeScaleSelect.appendChild(this.keyChangeScaleOptions[i]); + } + this.keyChangeSpan.appendChild(this.keyChangeScaleSelect); + + this.buttonKeyChangeListen = document.createElement("button"); + this.buttonKeyChangeListen.innerHTML = "Listen"; + this.buttonKeyChangeListen.className = "toolboxButton"; + this.keyChangeSpan.appendChild(this.buttonKeyChangeListen); + + this.keyChangeSpan.appendChild(document.createElement("br")); + this.keyChangeSpan.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(this.keyChangeSpan); + + + // Meter Change settings. + this.meterChangeSpan = document.createElement("span"); + this.meterChangeSpan.style.background = "#aaddff"; + + var meterChangeEditingLabelSpan = document.createElement("span"); + meterChangeEditingLabelSpan.innerHTML = "Edit Meter Change"; + meterChangeEditingLabelSpan.className = "toolboxLabel"; + this.meterChangeSpan.appendChild(meterChangeEditingLabelSpan); + this.meterChangeSpan.appendChild(document.createElement("br")); + + this.meterChangeNumeratorSelect = document.createElement("select"); + this.meterChangeNumeratorOptions = []; + for (var i = 0; i < 20; i++) + { + this.meterChangeNumeratorOptions[i] = document.createElement("option"); + this.meterChangeNumeratorOptions[i].innerHTML = "" + (i + 1); + this.meterChangeNumeratorSelect.appendChild(this.meterChangeNumeratorOptions[i]); + } + this.meterChangeSpan.appendChild(this.meterChangeNumeratorSelect); + + var meterChangeDividerSpan = document.createElement("span"); + meterChangeDividerSpan.innerHTML = " / "; + meterChangeDividerSpan.className = "toolboxText"; + this.meterChangeSpan.appendChild(meterChangeDividerSpan); + + this.meterChangeDenominatorSelect = document.createElement("select"); + this.meterChangeDenominatorOptions = []; + this.meterChangeDenominators = [ 2, 4, 8, 16 ]; + for (var i = 0; i < this.meterChangeDenominators.length; i++) + { + this.meterChangeDenominatorOptions[i] = document.createElement("option"); + this.meterChangeDenominatorOptions[i].innerHTML = "" + this.meterChangeDenominators[i]; + this.meterChangeDenominatorSelect.appendChild(this.meterChangeDenominatorOptions[i]); + } + this.meterChangeSpan.appendChild(this.meterChangeDenominatorSelect); + + this.meterChangeSpan.appendChild(document.createElement("br")); + this.meterChangeSpan.appendChild(document.createElement("br")); + this.toolsLayoutCell00.appendChild(this.meterChangeSpan); + + // Editing controls. this.labelKey = document.createElement("span"); this.labelKey.className = "toolboxLabel"; @@ -118,84 +206,9 @@ function Toolbox(div, editor) this.toolsLayoutCell00.appendChild(this.chordsSpan); - // Key Change settings. - this.keyChangeSpan = document.createElement("span"); - - var keyChangeEditingLabelSpan = document.createElement("span"); - keyChangeEditingLabelSpan.innerHTML = "Editing Key Change"; - keyChangeEditingLabelSpan.className = "toolboxLabel"; - this.keyChangeSpan.appendChild(keyChangeEditingLabelSpan); - this.keyChangeSpan.appendChild(document.createElement("br")); - - this.keyChangeTonicSelect = document.createElement("select"); - this.keyChangeTonicOptions = []; - for (var i = 0; i < 12; i++) - { - this.keyChangeTonicOptions[i] = document.createElement("option"); - this.keyChangeTonicSelect.appendChild(this.keyChangeTonicOptions[i]); - } - this.keyChangeSpan.appendChild(this.keyChangeTonicSelect); - - var keyChangeDividerSpan = document.createElement("span"); - keyChangeDividerSpan.innerHTML = " "; - keyChangeDividerSpan.className = "toolboxText"; - this.keyChangeSpan.appendChild(keyChangeDividerSpan); - - this.keyChangeScaleSelect = document.createElement("select"); - this.keyChangeScaleOptions = []; - for (var i = 0; i < theory.scales.length; i++) - { - this.keyChangeScaleOptions[i] = document.createElement("option"); - this.keyChangeScaleOptions[i].innerHTML = theory.scales[i].name; - this.keyChangeScaleSelect.appendChild(this.keyChangeScaleOptions[i]); - } - this.keyChangeSpan.appendChild(this.keyChangeScaleSelect); - - this.keyChangeSpan.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.keyChangeSpan); - - - // Meter Change settings. - this.meterChangeSpan = document.createElement("span"); - - var meterChangeEditingLabelSpan = document.createElement("span"); - meterChangeEditingLabelSpan.innerHTML = "Editing Meter Change"; - meterChangeEditingLabelSpan.className = "toolboxLabel"; - this.meterChangeSpan.appendChild(meterChangeEditingLabelSpan); - this.meterChangeSpan.appendChild(document.createElement("br")); - - this.meterChangeNumeratorSelect = document.createElement("select"); - this.meterChangeNumeratorOptions = []; - for (var i = 0; i < 20; i++) - { - this.meterChangeNumeratorOptions[i] = document.createElement("option"); - this.meterChangeNumeratorOptions[i].innerHTML = "" + (i + 1); - this.meterChangeNumeratorSelect.appendChild(this.meterChangeNumeratorOptions[i]); - } - this.meterChangeSpan.appendChild(this.meterChangeNumeratorSelect); - - var meterChangeDividerSpan = document.createElement("span"); - meterChangeDividerSpan.innerHTML = " / "; - meterChangeDividerSpan.className = "toolboxText"; - this.meterChangeSpan.appendChild(meterChangeDividerSpan); - - this.meterChangeDenominatorSelect = document.createElement("select"); - this.meterChangeDenominatorOptions = []; - this.meterChangeDenominators = [ 2, 4, 8, 16 ]; - for (var i = 0; i < this.meterChangeDenominators.length; i++) - { - this.meterChangeDenominatorOptions[i] = document.createElement("option"); - this.meterChangeDenominatorOptions[i].innerHTML = "" + this.meterChangeDenominators[i]; - this.meterChangeDenominatorSelect.appendChild(this.meterChangeDenominatorOptions[i]); - } - this.meterChangeSpan.appendChild(this.meterChangeDenominatorSelect); - - this.meterChangeSpan.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.meterChangeSpan); - - // Set up callbacks. this.editor = editor; + this.synth = synth; var that = this; editor.addOnCursorChanged(function() { that.refresh(); }); @@ -206,6 +219,90 @@ function Toolbox(div, editor) this.meterChangeNumeratorSelect.onchange = function() { that.editMeterChange(); }; this.meterChangeDenominatorSelect.onchange = function() { that.editMeterChange(); }; + this.buttonKeyChangeListen.onclick = function() + { + var keyChange = that.editor.getUniqueKeyChangeSelected(); + theory.playScaleSample(that.synth, keyChange.scale, keyChange.tonicPitch); + } + + this.buttonAddKeyChange.onclick = function(ev) + { + if (that.playing) + return; + + ev.preventDefault(); + var keyChange = new SongDataKeyChange(that.editor.cursorTick, theory.scales[0], theory.C); + that.editor.songData.addKeyChange(keyChange); + that.editor.unselectAll(); + that.editor.refreshRepresentation(); + that.editor.selectKeyChange(keyChange); + that.editor.refreshCanvas(); + that.refresh(); + that.refreshSelection(); + }; + + this.buttonAddMeterChange.onclick = function(ev) + { + if (that.playing) + return; + + ev.preventDefault(); + var meterChange = new SongDataMeterChange(that.editor.cursorTick, 4, 4); + that.editor.songData.addMeterChange(meterChange); + that.editor.unselectAll(); + that.editor.refreshRepresentation(); + that.editor.selectMeterChange(meterChange); + that.editor.refreshCanvas(); + that.refresh(); + that.refreshSelection(); + }; + + this.buttonPlay.onclick = function() + { + that.playing = !that.playing; + that.editor.setInteractionEnabled(!that.playing); + that.buttonPlay.innerHTML = (that.playing ? "■ Stop" : "▶ Play"); + + that.editor.showCursor = true; + that.editor.unselectAll(); + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.refresh(); + that.refreshSelection(); + + that.synth.stopAll(); + if (that.playing) + { + that.playTimer = setInterval(function() { that.processPlayback(12); }, 1000 / 30); + } + else + { + clearInterval(that.playTimer); + } + } + + this.buttonRewind.onclick = function() + { + if (that.playing) + { + clearInterval(that.playTimer); + that.synth.stopAll(); + } + + that.playing = false; + that.editor.setInteractionEnabled(true); + that.buttonPlay.innerHTML = (that.playing ? "■ Stop" : "▶ Play"); + + that.editor.cursorTick = 0; + that.editor.showCursor = true; + that.editor.unselectAll(); + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.refresh(); + that.refreshSelection(); + } + + this.refreshSelection(); this.refresh(); } @@ -272,11 +369,16 @@ Toolbox.prototype.refresh = function() var thisPitch = pitch; return function(ev) { + if (that.playing) + return; + ev.preventDefault(); - var note = new SongDataNote(that.editor.cursorTick, 25, thisPitch); + theory.playNoteSample(that.synth, thisPitch); + var note = new SongDataNote(that.editor.cursorTick, that.editor.WHOLE_NOTE_DURATION / 4, thisPitch); that.editor.songData.addNote(note); - that.editor.cursorTick += 25; + that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 4; that.editor.showCursor = true; + that.editor.unselectAll(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); @@ -319,11 +421,16 @@ Toolbox.prototype.refresh = function() var thisPitch = pitch; return function(ev) { + if (that.playing) + return; + ev.preventDefault(); - var chord = new SongDataChord(that.editor.cursorTick, 50, thisChord, thisPitch); + theory.playChordSample(that.synth, thisChord, thisPitch); + var chord = new SongDataChord(that.editor.cursorTick, that.editor.WHOLE_NOTE_DURATION / 2, thisChord, thisPitch); that.editor.songData.addChord(chord); - that.editor.cursorTick += 50; + that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 2; that.editor.showCursor = true; + that.editor.unselectAll(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); @@ -376,11 +483,16 @@ Toolbox.prototype.refresh = function() var thisPitch = pitch; return function(ev) { + if (that.playing) + return; + ev.preventDefault(); - var chord = new SongDataChord(that.editor.cursorTick, 50, thisChord, thisPitch); + theory.playChordSample(that.synth, thisChord, thisPitch); + var chord = new SongDataChord(that.editor.cursorTick, that.editor.WHOLE_NOTE_DURATION / 2, thisChord, thisPitch); that.editor.songData.addChord(chord); - that.editor.cursorTick += 50; + that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 2; that.editor.showCursor = true; + that.editor.unselectAll(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); @@ -398,10 +510,10 @@ Toolbox.prototype.refresh = function() Toolbox.prototype.refreshSelection = function() { - this.labelKey.style.display = "none"; - this.changesSpan.style.display = "none"; - this.notesSpan.style.display = "none"; - this.chordsSpan.style.display = "none"; + this.labelKey.style.display = "inline"; + this.changesSpan.style.display = "inline"; + this.notesSpan.style.display = "inline"; + this.chordsSpan.style.display = "inline"; this.keyChangeSpan.style.display = "none"; this.meterChangeSpan.style.display = "none"; @@ -429,7 +541,7 @@ Toolbox.prototype.refreshSelection = function() this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; - this.keyChangeSpan.style.display = "inline"; + this.keyChangeSpan.style.display = "block"; return; } @@ -443,20 +555,17 @@ Toolbox.prototype.refreshSelection = function() else if (meterChange.denominator == 8) this.meterChangeDenominatorSelect.selectedIndex = 2; else if (meterChange.denominator == 16) this.meterChangeDenominatorSelect.selectedIndex = 3; - this.meterChangeSpan.style.display = "inline"; + this.meterChangeSpan.style.display = "block"; return; } - - - this.labelKey.style.display = "inline"; - this.changesSpan.style.display = "inline"; - this.notesSpan.style.display = "inline"; - this.chordsSpan.style.display = "inline"; } Toolbox.prototype.editKeyChange = function() { + if (this.playing) + return; + var keyChange = this.editor.getUniqueKeyChangeSelected(); keyChange.scale = theory.scales[this.keyChangeScaleSelect.selectedIndex]; keyChange.tonicPitch = this.keyChangeTonicSelect.selectedIndex; @@ -470,6 +579,9 @@ Toolbox.prototype.editKeyChange = function() Toolbox.prototype.editMeterChange = function() { + if (this.playing) + return; + var meterChange = this.editor.getUniqueMeterChangeSelected(); meterChange.numerator = this.meterChangeNumeratorSelect.selectedIndex + 1; meterChange.denominator = this.meterChangeDenominators[this.meterChangeDenominatorSelect.selectedIndex]; @@ -478,4 +590,59 @@ Toolbox.prototype.editMeterChange = function() this.editor.selectMeterChange(meterChange); this.editor.refreshCanvas(); this.refresh(); +} + + +Toolbox.prototype.processPlayback = function(deltaTicks) +{ + var lastCursorTick = this.editor.cursorTick; + this.editor.cursorTick += deltaTicks; + this.editor.unselectAll(); + + for (var i = 0; i < this.editor.songData.notes.length; i++) + { + var note = this.editor.songData.notes[i]; + if (note.tick >= lastCursorTick && note.tick < this.editor.cursorTick) + { + this.synth.playNote(note.pitch + 60, note.duration * 2, 1); + } + + if (note.tick + note.duration >= lastCursorTick && note.tick < this.editor.cursorTick) + { + this.editor.noteSelections[i] = true; + } + } + + for (var i = 0; i < this.editor.songData.chords.length; i++) + { + var chord = this.editor.songData.chords[i]; + if (chord.tick + chord.duration > lastCursorTick && chord.tick < this.editor.cursorTick) + { + var halfTick = this.editor.WHOLE_NOTE_DURATION / 2; + var quarterTick = halfTick / 2; + var eighthTick = quarterTick / 2; + + var tick1 = (lastCursorTick - chord.tick); + var tick2 = (this.editor.cursorTick - chord.tick); + + if (Math.ceil(tick1 / halfTick) != Math.ceil(tick2 / halfTick)) + { + for (var j = 0; j < chord.chord.pitches.length; j++) + this.synth.playNote((chord.chord.pitches[j] + chord.rootPitch) % 12 + 60, quarterTick, 0.75); + } + else if (Math.ceil(tick1 / quarterTick) != Math.ceil(tick2 / quarterTick)) + { + for (var j = 1; j < chord.chord.pitches.length; j++) + this.synth.playNote((chord.chord.pitches[j] + chord.rootPitch) % 12 + 60, eighthTick, 0.5); + } + else if (Math.ceil(tick1 / eighthTick) != Math.ceil(tick2 / eighthTick)) + { + this.synth.playNote((chord.chord.pitches[0] + chord.rootPitch) % 12 + 60, eighthTick, 0.45); + } + + this.editor.chordSelections[i] = true; + } + } + + this.editor.refreshCanvas(); } \ No newline at end of file diff --git a/test.html b/test.html index cdb3488..86ef44a 100644 --- a/test.html +++ b/test.html @@ -21,5 +21,6 @@ + \ No newline at end of file diff --git a/test.js b/test.js index 1f00f5b..0169ba0 100644 --- a/test.js +++ b/test.js @@ -3,34 +3,45 @@ function testOnLoad() var song = new SongData(); song.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); song.addMeterChange(new SongDataMeterChange(0, 4, 4)); - song.addKeyChange(new SongDataKeyChange(100, theory.scales[1], theory.D)); - song.addMeterChange(new SongDataMeterChange(150, 3, 4)); - song.addKeyChange(new SongDataKeyChange(200, theory.scales[2], theory.B)); - song.addKeyChange(new SongDataKeyChange(300, theory.scales[4], theory.F)); + song.addKeyChange(new SongDataKeyChange(960 * 2, theory.scales[8], theory.C)); + song.addKeyChange(new SongDataKeyChange(960 * 4, theory.scales[0], theory.C)); - song.addChord(new SongDataChord(0, 100, theory.chords[0], theory.C)); - song.addChord(new SongDataChord(100, 50, theory.chords[1], theory.C)); - song.addChord(new SongDataChord(150, 50, theory.chords[2], theory.C)); - song.addChord(new SongDataChord(200, 150, theory.chords[3], theory.C)); - song.addNote(new SongDataNote(0, 300, 12)); - song.addNote(new SongDataNote(0, 300, 2)); - song.addNote(new SongDataNote(0, 300, 7)); - song.addNote(new SongDataNote(325, 25, 0)); - song.addNote(new SongDataNote(350, 25, 1)); - song.addNote(new SongDataNote(375, 25, 2)); - song.addNote(new SongDataNote(400, 25, 3)); - song.addNote(new SongDataNote(425, 25, 4)); - song.addNote(new SongDataNote(450, 25, 5)); - song.addNote(new SongDataNote(475, 25, 6)); - song.addNote(new SongDataNote(500, 25, 7)); - song.addNote(new SongDataNote(525, 25, 8)); - song.addNote(new SongDataNote(550, 25, 9)); - song.addNote(new SongDataNote(575, 25, 10)); - song.addNote(new SongDataNote(600, 25, 11)); + song.addChord(new SongDataChord(960 * 0.0, 960 / 2, theory.chords[0], theory.C)); + song.addChord(new SongDataChord(960 * 0.5, 960 / 2, theory.chords[0], theory.F)); + song.addChord(new SongDataChord(960 * 1.0, 960 / 1, theory.chords[0], theory.G)); + song.addChord(new SongDataChord(960 * 2.0, 960 / 2, theory.chords[0], theory.C)); + song.addChord(new SongDataChord(960 * 2.5, 960 / 2, theory.chords[1], theory.F)); + song.addChord(new SongDataChord(960 * 3.0, 960 / 2, theory.chords[6], theory.C)); + song.addChord(new SongDataChord(960 * 3.5, 960 / 2, theory.chords[4], theory.G)); + song.addChord(new SongDataChord(960 * 4.0, 960 * 2, theory.chords[0], theory.C)); + + song.addNote(new SongDataNote(0, 960 / 4, theory.C)); + song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.F)); + song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E)); + song.addNote(new SongDataNote(960 / 4 + 960 / 16 * 2, 960 / 4, theory.F)); + song.addNote(new SongDataNote(960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E)); + song.addNote(new SongDataNote(960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.D)); + song.addNote(new SongDataNote(960, 960, theory.G)); + + song.addNote(new SongDataNote(960 * 2, 960 / 4, theory.C)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4, 960 / 16, theory.F)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16, 960 / 16, theory.E)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16 * 2, 960 / 4, theory.F)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.Cs)); + song.addNote(new SongDataNote(960 * 2 + 960, 960, theory.C)); + + song.addNote(new SongDataNote(960 * 2.5, 960 / 16, theory.Gs)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16, 960 / 16, theory.C + 12)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 2, 960 / 16, theory.Cs + 12)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 12)); + + var synth = new Synth(); + setInterval(function() { synth.process(12); }, 1000 / 30); var canvas = document.getElementById("editorCanvas"); - var editor = new SongEditor(canvas, song); + var editor = new SongEditor(canvas, song, synth); editor.refreshCanvas(); - var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor); + var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor, synth); } \ No newline at end of file From fc7749992bfecddda0d77cd56734aacfbdc8b3f8 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Mon, 30 Nov 2015 19:04:21 -0200 Subject: [PATCH 13/46] make playback aware of bpm; add bpm control --- src/editor.js | 2 +- src/toolbox.js | 71 +++++++++++++++++++++++++++++++++++++++++++------- test.js | 2 +- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/editor.js b/src/editor.js index 1d4f546..094c09b 100644 --- a/src/editor.js +++ b/src/editor.js @@ -25,7 +25,7 @@ function SongEditor(canvas, songData, synth) this.selectedObjects = 0; // The scaling from ticks to pixels. - this.tickZoom = 0.1; + this.tickZoom = 0.15; // The tick grid which the cursor is snapped to. this.tickSnap = 960 / 16; diff --git a/src/toolbox.js b/src/toolbox.js index 5173688..9d03e1f 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -3,6 +3,7 @@ function Toolbox(div, editor, synth) this.div = div; this.playing = false; this.playTimer = null; + this.playTimerRefresh = null; this.initCSS(); @@ -37,6 +38,17 @@ function Toolbox(div, editor, synth) this.buttonRewind.className = "toolboxButton"; this.mainLayoutCell00.appendChild(this.buttonRewind); + this.mainLayoutCell00.appendChild(document.createElement("br")); + + var inputBPMSpan = document.createElement("span"); + inputBPMSpan.innerHTML = "Tempo "; + inputBPMSpan.className = "toolboxText"; + this.mainLayoutCell00.appendChild(inputBPMSpan); + + this.inputBPM = document.createElement("input"); + this.inputBPM.value = editor.songData.beatsPerMinute; + this.inputBPM.style.width = "30px"; + this.mainLayoutCell00.appendChild(this.inputBPM); // Key Change settings. this.keyChangeSpan = document.createElement("span"); @@ -273,11 +285,13 @@ function Toolbox(div, editor, synth) that.synth.stopAll(); if (that.playing) { - that.playTimer = setInterval(function() { that.processPlayback(12); }, 1000 / 30); + that.playTimer = setInterval(function() { that.processPlayback(); }, 1000 / 60); + that.playTimerRefresh = setInterval(function() { that.processPlaybackRefresh(); }, 1000 / 15); } else { clearInterval(that.playTimer); + clearInterval(that.playTimerRefresh); } } @@ -286,6 +300,7 @@ function Toolbox(div, editor, synth) if (that.playing) { clearInterval(that.playTimer); + clearInterval(that.playTimerRefresh); that.synth.stopAll(); } @@ -302,6 +317,23 @@ function Toolbox(div, editor, synth) that.refreshSelection(); } + this.inputBPM.onchange = function() + { + var bpm = parseInt(that.inputBPM.value); + + if (bpm != bpm) bpm = 120; // Test for NaN. + if (bpm < 1) bpm = 1; + if (bpm > 400) bpm = 400; + bpm -= (bpm % 1); + + that.editor.songData.beatsPerMinute = bpm; + } + + this.inputBPM.onblur = function() + { + that.inputBPM.value = that.editor.songData.beatsPerMinute; + } + this.refreshSelection(); this.refresh(); } @@ -593,12 +625,15 @@ Toolbox.prototype.editMeterChange = function() } -Toolbox.prototype.processPlayback = function(deltaTicks) +Toolbox.prototype.processPlayback = function() { + var bpm = this.editor.songData.beatsPerMinute; + var deltaTicks = bpm / 60 / 60 * (960 / 4); + var lastCursorTick = this.editor.cursorTick; this.editor.cursorTick += deltaTicks; - this.editor.unselectAll(); + // TODO: Use binary search. for (var i = 0; i < this.editor.songData.notes.length; i++) { var note = this.editor.songData.notes[i]; @@ -606,11 +641,6 @@ Toolbox.prototype.processPlayback = function(deltaTicks) { this.synth.playNote(note.pitch + 60, note.duration * 2, 1); } - - if (note.tick + note.duration >= lastCursorTick && note.tick < this.editor.cursorTick) - { - this.editor.noteSelections[i] = true; - } } for (var i = 0; i < this.editor.songData.chords.length; i++) @@ -639,10 +669,31 @@ Toolbox.prototype.processPlayback = function(deltaTicks) { this.synth.playNote((chord.chord.pitches[0] + chord.rootPitch) % 12 + 60, eighthTick, 0.45); } - - this.editor.chordSelections[i] = true; } } +} + + + + +Toolbox.prototype.processPlaybackRefresh = function() +{ + this.editor.unselectAll(); + + // TODO: Use binary search. + for (var i = 0; i < this.editor.songData.notes.length; i++) + { + var note = this.editor.songData.notes[i]; + if (note.tick + note.duration >= this.editor.cursorTick && note.tick < this.editor.cursorTick) + this.editor.noteSelections[i] = true; + } + + for (var i = 0; i < this.editor.songData.chords.length; i++) + { + var chord = this.editor.songData.chords[i]; + if (chord.tick + chord.duration > this.editor.cursorTick && chord.tick < this.editor.cursorTick) + this.editor.chordSelections[i] = true; + } this.editor.refreshCanvas(); } \ No newline at end of file diff --git a/test.js b/test.js index 0169ba0..33da026 100644 --- a/test.js +++ b/test.js @@ -37,7 +37,7 @@ function testOnLoad() song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 12)); var synth = new Synth(); - setInterval(function() { synth.process(12); }, 1000 / 30); + setInterval(function() { synth.process(6); }, 1000 / 60); var canvas = document.getElementById("editorCanvas"); var editor = new SongEditor(canvas, song, synth); From 6c23be3e1bef6d4c48d4e8f7d3e9bb1c608b8280 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Wed, 2 Dec 2015 15:54:22 -0200 Subject: [PATCH 14/46] add vertical scrolling; add cursor zones; fix some errors with rows/pitches theory --- src/editor.js | 10 ++- src/editor_drawing.js | 137 +++++++++++++++++++++++++++-------- src/editor_interaction.js | 44 ++++++++++- src/editor_representation.js | 15 +++- src/theory.js | 33 ++++++++- src/toolbox.js | 9 ++- test.js | 36 ++++----- 7 files changed, 224 insertions(+), 60 deletions(-) diff --git a/src/editor.js b/src/editor.js index 094c09b..8c8cd62 100644 --- a/src/editor.js +++ b/src/editor.js @@ -38,9 +38,17 @@ function SongEditor(canvas, songData, synth) this.viewKeyChanges = []; this.viewMeterChanges = []; + // These control scrolling. + this.rowAtCenter = 8 * 5; + // These control cursor (the vertical blue bar) interaction. + this.CURSOR_ZONE_ALL = 0; + this.CURSOR_ZONE_NOTES = 1; + this.CURSOR_ZONE_CHORDS = 2; + this.cursorTick = 0; this.showCursor = true; + this.cursorZone = this.CURSOR_ZONE_ALL; // These control mouse interaction. this.interactionEnabled = true; @@ -73,7 +81,7 @@ function SongEditor(canvas, songData, synth) this.CHORD_HEIGHT = 60; this.CHORDNOTE_MARGIN = 10; this.CHORD_ORNAMENT_HEIGHT = 5; - this.KEYCHANGE_BAR_WIDTH = 20; + this.KEYCHANGE_BAR_WIDTH = 27; this.METERCHANGE_BAR_WIDTH = 4; // Finally, set the song data, and the synth manager. diff --git a/src/editor_drawing.js b/src/editor_drawing.js index edc21ec..3c539d7 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -12,24 +12,35 @@ SongEditor.prototype.refreshCanvas = function() var CURSOR_COLOR = "#0000ff"; var BLOCK_BORDER_COLOR = "#000000"; var BLOCK_MEASURE_COLOR = "#dddddd"; - var BLOCK_MEASURE_COLOR_STRONG = "#bbbbbb"; + var BLOCK_MEASURE_COLOR_STRONG = "#888888"; for (var i = 0; i < this.viewBlocks.length; i++) { var block = this.viewBlocks[i]; + var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); + var firstVisibleRowY = this.getYForRow(block, visibleRows.first); + var lastVisibleRowY = this.getYForRow(block, visibleRows.last); + + this.ctx.save(); + this.ctx.beginPath(); + + var clipY1 = Math.max(block.y1, lastVisibleRowY); + var clipY2 = Math.min(block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN, firstVisibleRowY); + this.ctx.rect(0, clipY1, this.canvasWidth, clipY2 - clipY1); + this.ctx.clip(); // Draw rows. - for (var row = 0; row < 14; row++) + for (var row = visibleRows.first; row <= visibleRows.last; row++) { - this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key) == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); + this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key) % 12 == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); this.ctx.lineWidth = 2; this.ctx.beginPath(); - this.ctx.moveTo(block.x1, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - row * this.NOTE_HEIGHT); - this.ctx.lineTo(block.x2, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - row * this.NOTE_HEIGHT); + this.ctx.moveTo(block.x1, this.getYForRow(block, row)); + this.ctx.lineTo(block.x2, this.getYForRow(block, row)); this.ctx.stroke(); } - // Draw measures. + // Draw note measures. var submeasureCount = 0; for (var n = block.meter.tick - block.tick; n < block.duration; n += this.WHOLE_NOTE_DURATION / block.meter.denominator) { @@ -40,9 +51,6 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.beginPath(); this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y1); this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN); - - this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT); - this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); this.ctx.stroke(); } @@ -55,28 +63,63 @@ SongEditor.prototype.refreshCanvas = function() var noteIndex = block.notes[n].noteIndex; var note = this.songData.notes[noteIndex]; - if (this.noteSelections[noteIndex] && this.mouseDragAction != null) + if (!this.noteSelections[noteIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") + this.drawNote(i, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); + } + + if (this.mouseDragAction != null && this.mouseDragAction != "scroll") + { + for (var n = 0; n < this.songData.notes.length; n++) { - var draggedNote = this.getNoteDragged(note, this.mouseDragCurrent); - this.drawNote(i, draggedNote.pitch, draggedNote.tick, draggedNote.duration, noteIndex == this.hoverNote, true); + var note = this.songData.notes[n]; + if (this.noteSelections[n]) + { + var draggedNote = this.getNoteDragged(note, this.mouseDragCurrent); + this.drawNote(i, draggedNote.pitch, draggedNote.tick, draggedNote.duration, noteIndex == this.hoverNote, true); + } } - else - this.drawNote(i, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); } - // Draw chord. + this.ctx.restore(); + + // Draw chord measures. + var submeasureCount = 0; + for (var n = block.meter.tick - block.tick; n < block.duration; n += this.WHOLE_NOTE_DURATION / block.meter.denominator) + { + if (n >= 0) + { + this.ctx.strokeStyle = (submeasureCount == 0 ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT); + this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); + this.ctx.stroke(); + } + + submeasureCount = (submeasureCount + 1) % block.meter.numerator; + } + + // Draw chords. for (var n = 0; n < block.chords.length; n++) { var chordIndex = block.chords[n].chordIndex; var chord = this.songData.chords[chordIndex]; - if (this.chordSelections[chordIndex] && this.mouseDragAction != null) + if (!this.chordSelections[chordIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") + this.drawChord(i, chord, chord.tick, chord.duration, chordIndex == this.hoverChord, this.chordSelections[chordIndex]); + } + + if (this.mouseDragAction != null && this.mouseDragAction != "scroll") + { + for (var n = 0; n < this.songData.chords.length; n++) { - var draggedChord = this.getChordDragged(chord, this.mouseDragCurrent); - this.drawChord(i, chord, draggedChord.tick, draggedChord.duration, chordIndex == this.hoverChord, true); + var chord = this.songData.chords[n]; + if (this.chordSelections[n]) + { + var draggedChord = this.getChordDragged(chord, this.mouseDragCurrent); + this.drawChord(i, chord, draggedChord.tick, draggedChord.duration, chordIndex == this.hoverChord, true); + } } - else - this.drawChord(i, chord, chord.tick, chord.duration, chordIndex == this.hoverChord, this.chordSelections[chordIndex]); } // Draw borders. @@ -98,6 +141,12 @@ SongEditor.prototype.refreshCanvas = function() var cursorY1 = block.y1; var cursorY2 = block.y2; + if (this.cursorZone == this.CURSOR_ZONE_NOTES) + cursorY2 = block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + + else if (this.cursorZone == this.CURSOR_ZONE_CHORDS) + cursorY1 = block.y2 - this.CHORD_HEIGHT; + this.ctx.beginPath(); this.ctx.moveTo(cursorX, cursorY1); this.ctx.lineTo(cursorX, cursorY2); @@ -192,13 +241,23 @@ SongEditor.prototype.refreshCanvas = function() // Draw pitches. this.ctx.font = "10px Tahoma"; - for (var row = 0; row < 13; row++) + + var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); + for (var row = visibleRows.first; row < visibleRows.last; row++) { + var pitch = theory.getPitchForRow(row, songKeyChange); + var y = this.getYForRow(this.viewBlocks[keyChange.blockIndex], row + 1); + this.ctx.fillStyle = KEY_PITCH_COLOR; this.ctx.fillText( - "" + theory.getNameForPitch(theory.getPitchForRow(row, songKeyChange)), + "" + theory.getNameForPitch(pitch), keyChange.x1 + 4, - this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row + 1) * this.NOTE_HEIGHT); + y); + + this.ctx.fillText( + "" + theory.getOctaveForPitch(pitch), + keyChange.x1 + 18, + y); } } @@ -284,6 +343,8 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove if (tick + duration <= block.tick || tick >= block.tick + block.duration) return; + + var scrollY = -this.rowAtCenter * this.NOTE_HEIGHT; var deg = theory.getDegreeForPitch(pitch, block.key); var row = theory.getRowForPitch(pitch, block.key); @@ -315,9 +376,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove { var nextBlock = this.viewBlocks[blockIndex + 1]; var nextRow = theory.getRowForPitch(pitch, nextBlock.key); - - var nextY1 = nextBlock.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (nextRow + 1) * this.NOTE_HEIGHT; - var nextY2 = nextY1 + this.NOTE_HEIGHT; + var nextPos = this.getNotePosition(block, nextRow, tick, duration); var col = theory.getColorForDegree(deg - (deg % 1)); @@ -326,8 +385,8 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove this.ctx.fillStyle = col; this.ctx.beginPath(); this.ctx.moveTo(block.x2, pos.y1); - this.ctx.lineTo(nextBlock.x1, nextY1); - this.ctx.lineTo(nextBlock.x1, nextY2); + this.ctx.lineTo(nextBlock.x1, nextPos.y1); + this.ctx.lineTo(nextBlock.x1, nextPos.y2); this.ctx.lineTo(block.x2, pos.y2); this.ctx.fill(); this.ctx.restore(); @@ -401,7 +460,7 @@ SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hov this.ctx.restore(); - // Draw possibly-bent part between blocks. + // Draw part between blocks. if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) { var nextBlock = this.viewBlocks[blockIndex + 1]; @@ -429,7 +488,7 @@ SongEditor.prototype.drawDegreeColoredRectangle = function(deg, pos) this.ctx.fillStyle = col; this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - // Draw stripes on fractional scale degrees. + // Draw stripes for fractional scale degrees. if ((deg % 1) != 0) { this.ctx.strokeStyle = theory.getColorForDegree((deg - (deg % 1) + 1) % 7); @@ -444,4 +503,22 @@ SongEditor.prototype.drawDegreeColoredRectangle = function(deg, pos) } this.ctx.restore(); +} + + +SongEditor.prototype.getYForRow = function(block, row) +{ + var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + return block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - noteAreaHeight / 2 + (this.rowAtCenter - row) * this.NOTE_HEIGHT; +} + + +SongEditor.prototype.getFirstAndLastVisibleRowsForBlock = function(block) +{ + var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + + return { + first: Math.max(Math.floor(this.rowAtCenter - noteAreaHeight / 2 / this.NOTE_HEIGHT), theory.getRowForPitch(theory.getMinPitch(), block.key)), + last: Math.min(Math.ceil(this.rowAtCenter + noteAreaHeight / 2 / this.NOTE_HEIGHT), theory.getRowForPitch(theory.getMaxPitch(), block.key)) + }; } \ No newline at end of file diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 6ae1f7b..cb5802b 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -132,6 +132,21 @@ SongEditor.prototype.getTickAtPosition = function(x) } +SongEditor.prototype.getZoneAtPosition = function(y) +{ + if (y >= this.MARGIN_TOP + this.HEADER_MARGIN && + y <= this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN) + return this.CURSOR_ZONE_NOTES; + + else if (y >= this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT && + y <= this.canvasHeight - this.MARGIN_BOTTOM) + return this.CURSOR_ZONE_CHORDS; + + else + return this.CURSOR_ZONE_ALL; +} + + SongEditor.prototype.getKeyAtTick = function(tick) { for (var b = 0; b < this.viewBlocks.length; b++) @@ -179,7 +194,7 @@ SongEditor.prototype.getEarliestSelectedTick = function() SongEditor.prototype.getLatestSelectedTick = function() { - // FIXME: Take key/meter changes into consideration. + // TODO: Take key/meter changes into consideration. // TODO: Use binary search. var latest = 0; for (var n = this.noteSelections.length - 1; n >= 0; n--) @@ -414,13 +429,30 @@ SongEditor.prototype.handleMouseMove = function(ev) } } } + + this.refreshCanvas(); } + else if (this.mouseDragAction == "move") + { this.canvas.style.cursor = "move"; + this.refreshCanvas(); + } + else if (this.mouseDragAction == "stretch") + { this.canvas.style.cursor = "ew-resize"; + this.refreshCanvas(); + } - this.refreshCanvas(); + else if (this.mouseDragAction == "scroll") + { + var rowOffset = (mousePos.y - this.mouseDragOrigin.y) / this.NOTE_HEIGHT; + this.rowAtCenter += rowOffset; + this.mouseDragOrigin.y = mousePos.y; + this.refreshRepresentation(); + this.refreshCanvas(); + } } @@ -439,6 +471,7 @@ SongEditor.prototype.handleMouseDown = function(ev) this.mouseDragOrigin = mousePos; this.cursorTick = this.getTickAtPosition(mousePos.x); + this.cursorZone = this.getZoneAtPosition(mousePos.y); this.showCursor = true; // Start a dragging operation. @@ -510,6 +543,11 @@ SongEditor.prototype.handleMouseDown = function(ev) this.meterChangeSelections[this.hoverMeterChange] = true; this.mouseDragAction = "move"; } + else + { + this.mouseDragAction = "scroll"; + this.clearHover(); + } this.mouseDown = true; this.callOnSelectionChanged(); @@ -527,7 +565,7 @@ SongEditor.prototype.handleMouseUp = function(ev) return; // Apply dragged modifications. - if (this.mouseDown && this.mouseDragAction != null) + if (this.mouseDown && this.mouseDragAction != null && this.mouseDragAction != "scroll") { // Store modified objects in a local array and // remove them from the song data. diff --git a/src/editor_representation.js b/src/editor_representation.js index 0955416..9453a29 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -151,6 +151,10 @@ SongEditor.prototype.refreshRepresentation = function() for (var n = 0; n < this.songData.notes.length; n++) { var note = this.songData.notes[n]; + + if (note.tick + note.duration <= block.tick || note.tick >= block.tick + block.duration) + continue; + var noteRow = theory.getRowForPitch(note.pitch, block.key); var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); block.notes.push( @@ -171,6 +175,10 @@ SongEditor.prototype.refreshRepresentation = function() for (var n = 0; n < this.songData.chords.length; n++) { var chord = this.songData.chords[n]; + + if (chord.tick + chord.duration <= block.tick || chord.tick >= block.tick + block.duration) + continue; + var chordPos = this.getChordPosition(block, chord.tick, chord.duration); block.chords.push( { @@ -228,13 +236,16 @@ SongEditor.prototype.refreshRepresentation = function() SongEditor.prototype.getNotePosition = function(block, row, tick, duration) { var blockTick = tick - block.tick; + var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + var firstRow = block.key.scale.degrees.length * 3; + var rowY = this.getYForRow(block, row); return { resizeHandleL: block.x1 + blockTick * this.tickZoom, resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), - y1: block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row + 1) * this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, - y2: block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - (row) * this.NOTE_HEIGHT - this.NOTE_MARGIN_VER + y1: rowY - this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, + y2: rowY - this.NOTE_MARGIN_VER }; } diff --git a/src/theory.js b/src/theory.js index 7199021..76b8cf6 100644 --- a/src/theory.js +++ b/src/theory.js @@ -84,6 +84,18 @@ function Theory() { name: "Augmented Major Seventh", roman: "X", romanSup: "+(M7)", romanSub: "", pitches: [ C, E, Gs, B ] } ]; + + this.getMinPitch = function() + { + return 60 - 12 * 3; + } + + + this.getMaxPitch = function() + { + return 60 + 12 * 3; + } + this.getNameForPitch = function(pitch, scale) { @@ -100,6 +112,18 @@ function Theory() return numerals[(pitch + 12 - key.tonicPitch) % 12]; }; + + this.getOctaveForPitch = function(pitch) + { + return Math.floor(pitch / 12); + }; + + + this.getOctaveForDegree = function(degree, key) + { + return Math.floor(degree / key.scale.degrees.length); + }; + this.getColorForDegree = function(degree) { @@ -148,6 +172,7 @@ function Theory() // indicates that the pitch falls between scale degrees. this.getRowForPitch = function(pitch, key) { + var pitchOctave = theory.getOctaveForPitch(pitch); var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); var pitchDegree = key.scale.degrees.length - 0.5; @@ -190,7 +215,7 @@ function Theory() key.tonicPitch = originalTonicPitch; } - return key.scale.degrees[(row + key.scale.degrees.length - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch; + return key.scale.degrees[(row + key.scale.degrees.length * 3 - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch + Math.floor((row - degreeOffsetFromC) / key.scale.degrees.length) * 12; }; @@ -228,7 +253,7 @@ function Theory() // Plays a sample of the given note. this.playNoteSample = function(synth, pitch) { - synth.playNote(pitch + 60, 960 / 8, 1); + synth.playNote(pitch, 960 / 8, 1); } @@ -247,8 +272,8 @@ function Theory() { for (var k = 0; k < scale.degrees.length; k++) { - synth.playNoteDelayed((tonicPitch + scale.degrees[k]) + 60, k * 5, 960 / 8, 1); + synth.playNoteDelayed((tonicPitch + scale.degrees[k]) + 60, k * 60, 960 / 8, 1); } - synth.playNoteDelayed((tonicPitch + scale.degrees[0] + 12) + 60, scale.degrees.length * 5, 960 / 8, 1); + synth.playNoteDelayed((tonicPitch + scale.degrees[0] + 12) + 60, scale.degrees.length * 60, 960 / 8, 1); } } diff --git a/src/toolbox.js b/src/toolbox.js index 9d03e1f..f894a5e 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -277,6 +277,7 @@ function Toolbox(div, editor, synth) that.editor.showCursor = true; that.editor.unselectAll(); + that.editor.clearHover(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); @@ -285,6 +286,7 @@ function Toolbox(div, editor, synth) that.synth.stopAll(); if (that.playing) { + that.editor.cursorZone = that.editor.CURSOR_ZONE_ALL; that.playTimer = setInterval(function() { that.processPlayback(); }, 1000 / 60); that.playTimerRefresh = setInterval(function() { that.processPlaybackRefresh(); }, 1000 / 15); } @@ -410,12 +412,13 @@ Toolbox.prototype.refresh = function() that.editor.songData.addNote(note); that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 4; that.editor.showCursor = true; + that.editor.cursorZone = that.editor.CURSOR_ZONE_NOTES; that.editor.unselectAll(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); } - }(pitch); + }(pitch + 60); } var chordCategory = this.chordSelect.selectedIndex; @@ -462,6 +465,7 @@ Toolbox.prototype.refresh = function() that.editor.songData.addChord(chord); that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 2; that.editor.showCursor = true; + that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; that.editor.unselectAll(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); @@ -524,6 +528,7 @@ Toolbox.prototype.refresh = function() that.editor.songData.addChord(chord); that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 2; that.editor.showCursor = true; + that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; that.editor.unselectAll(); that.editor.refreshRepresentation(); that.editor.refreshCanvas(); @@ -639,7 +644,7 @@ Toolbox.prototype.processPlayback = function() var note = this.editor.songData.notes[i]; if (note.tick >= lastCursorTick && note.tick < this.editor.cursorTick) { - this.synth.playNote(note.pitch + 60, note.duration * 2, 1); + this.synth.playNote(note.pitch, note.duration * 2, 1); } } diff --git a/test.js b/test.js index 33da026..6621338 100644 --- a/test.js +++ b/test.js @@ -15,26 +15,26 @@ function testOnLoad() song.addChord(new SongDataChord(960 * 3.5, 960 / 2, theory.chords[4], theory.G)); song.addChord(new SongDataChord(960 * 4.0, 960 * 2, theory.chords[0], theory.C)); - song.addNote(new SongDataNote(0, 960 / 4, theory.C)); - song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.F)); - song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E)); - song.addNote(new SongDataNote(960 / 4 + 960 / 16 * 2, 960 / 4, theory.F)); - song.addNote(new SongDataNote(960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E)); - song.addNote(new SongDataNote(960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.D)); - song.addNote(new SongDataNote(960, 960, theory.G)); + song.addNote(new SongDataNote(0, 960 / 4, theory.C + 60)); + song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.F + 60)); + song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); + song.addNote(new SongDataNote(960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); + song.addNote(new SongDataNote(960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E + 60)); + song.addNote(new SongDataNote(960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.D + 60)); + song.addNote(new SongDataNote(960, 960, theory.G + 60)); - song.addNote(new SongDataNote(960 * 2, 960 / 4, theory.C)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4, 960 / 16, theory.F)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16, 960 / 16, theory.E)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16 * 2, 960 / 4, theory.F)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.Cs)); - song.addNote(new SongDataNote(960 * 2 + 960, 960, theory.C)); + song.addNote(new SongDataNote(960 * 2, 960 / 4, theory.C + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4, 960 / 16, theory.F + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.Cs + 60)); + song.addNote(new SongDataNote(960 * 2 + 960, 960, theory.C + 60)); - song.addNote(new SongDataNote(960 * 2.5, 960 / 16, theory.Gs)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16, 960 / 16, theory.C + 12)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 2, 960 / 16, theory.Cs + 12)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 12)); + song.addNote(new SongDataNote(960 * 2.5, 960 / 16, theory.Gs + 60)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16, 960 / 16, theory.C + 60 + 12)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 2, 960 / 16, theory.Cs + 60 + 12)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 60 + 12)); var synth = new Synth(); setInterval(function() { synth.process(6); }, 1000 / 60); From ef1c29a7fdd53236efe29b7af981fadcd6d5136a Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 3 Dec 2015 08:29:48 -0200 Subject: [PATCH 15/46] add backspace deleting; fix scrolling bugs; create note/chord clipping functions; change html filename for gh-pages --- test.html => index.html | 6 +-- test.js => main.js | 0 src/editor_drawing.js | 6 +-- src/editor_interaction.js | 111 +++++++++++++++++++++++++++++++++++++- src/songdata.js | 109 ++++++++++++++++++++----------------- src/theory.js | 4 +- src/toolbox.js | 4 +- 7 files changed, 181 insertions(+), 59 deletions(-) rename test.html => index.html (81%) rename test.js => main.js (100%) diff --git a/test.html b/index.html similarity index 81% rename from test.html rename to index.html index 86ef44a..ac3e48a 100644 --- a/test.html +++ b/index.html @@ -3,17 +3,17 @@ - Test + Music Analysis - +
- + diff --git a/test.js b/main.js similarity index 100% rename from test.js rename to main.js diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 3c539d7..02b97f3 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -19,7 +19,7 @@ SongEditor.prototype.refreshCanvas = function() var block = this.viewBlocks[i]; var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); var firstVisibleRowY = this.getYForRow(block, visibleRows.first); - var lastVisibleRowY = this.getYForRow(block, visibleRows.last); + var lastVisibleRowY = this.getYForRow(block, visibleRows.last + 1); this.ctx.save(); this.ctx.beginPath(); @@ -30,7 +30,7 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.clip(); // Draw rows. - for (var row = visibleRows.first; row <= visibleRows.last; row++) + for (var row = visibleRows.first; row <= visibleRows.last + 1; row++) { this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key) % 12 == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); this.ctx.lineWidth = 2; @@ -243,7 +243,7 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.font = "10px Tahoma"; var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); - for (var row = visibleRows.first; row < visibleRows.last; row++) + for (var row = visibleRows.first; row <= visibleRows.last; row++) { var pitch = theory.getPitchForRow(row, songKeyChange); var y = this.getYForRow(this.viewBlocks[keyChange.blockIndex], row + 1); diff --git a/src/editor_interaction.js b/src/editor_interaction.js index cb5802b..71e159d 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -221,6 +221,18 @@ SongEditor.prototype.getLatestSelectedTick = function() } +SongEditor.prototype.getFirstAndLastScrollableRows = function() +{ + var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + + // FIXME: Hardcoded values. + return { + first: Math.floor(7 * 9 - noteAreaHeight / 2 / this.NOTE_HEIGHT), + last: Math.ceil(7 * 2 + noteAreaHeight / 2 / this.NOTE_HEIGHT) + }; +} + + SongEditor.prototype.getNoteDragged = function(note, dragPosition) { var dragTick = this.getTickAtPosition(dragPosition.x); @@ -232,7 +244,7 @@ SongEditor.prototype.getNoteDragged = function(note, dragPosition) return { tick: Math.max(0, note.tick + tickOffset), duration: note.duration, - pitch: note.pitch + pitchOffset + pitch: Math.max(theory.getMinPitch(), Math.min(theory.getMaxPitch(), note.pitch + pitchOffset)) }; } else if (this.mouseDragAction == "stretch") @@ -449,6 +461,8 @@ SongEditor.prototype.handleMouseMove = function(ev) { var rowOffset = (mousePos.y - this.mouseDragOrigin.y) / this.NOTE_HEIGHT; this.rowAtCenter += rowOffset; + var rowLimits = this.getFirstAndLastScrollableRows(); + this.rowAtCenter = Math.min(rowLimits.first, Math.max(rowLimits.last, this.rowAtCenter)); this.mouseDragOrigin.y = mousePos.y; this.refreshRepresentation(); this.refreshCanvas(); @@ -712,7 +726,7 @@ SongEditor.prototype.handleKeyDown = function(ev) var keyCode = (ev.key || ev.which || ev.keyCode); - if (keyCode == 46 || keyCode == 8) // Delete/Backspace + if (keyCode == 46 || (keyCode == 8 && this.selectedObjects > 0)) // Delete/Backspace { // Remove selected objects from the song data. var selectedNotes = []; @@ -761,4 +775,97 @@ SongEditor.prototype.handleKeyDown = function(ev) this.refreshCanvas(); this.callOnSelectionChanged(); } + + else if (keyCode == 8 && this.selectedObjects == 0) // Backspace + { + var lastTickToConsider = 0; + var considerNotes = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_NOTES); + var considerChords = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_CHORDS); + + if (considerNotes) + { + for (var i = 0; i < this.songData.notes.length; i++) + { + var note = this.songData.notes[i]; + if (note.tick + note.duration > lastTickToConsider) + lastTickToConsider = Math.min(note.tick + note.duration, this.cursorTick); + } + } + + if (considerChords) + { + for (var i = 0; i < this.songData.chords.length; i++) + { + var chord = this.songData.chords[i]; + if (chord.tick + chord.duration > lastTickToConsider) + lastTickToConsider = Math.min(chord.tick + chord.duration, this.cursorTick); + } + } + + if (lastTickToConsider != this.cursorTick) + { + this.cursorTick = Math.max(0, lastTickToConsider); + + this.clearHover(); + this.refreshCanvas(); + this.callOnCursorChanged(); + return; + } + + var deleteBeginTick = 0; + + if (considerNotes) + { + for (var i = 0; i < this.songData.notes.length; i++) + { + var note = this.songData.notes[i]; + if (note.tick + note.duration > lastTickToConsider) + continue; + + if (note.tick + note.duration == lastTickToConsider && + note.tick > deleteBeginTick) + { + deleteBeginTick = note.tick; + } + else if (note.tick + note.duration != lastTickToConsider && + note.tick + note.duration > deleteBeginTick) + { + deleteBeginTick = note.tick + note.duration; + } + } + } + + if (considerChords) + { + for (var i = 0; i < this.songData.chords.length; i++) + { + var chord = this.songData.chords[i]; + if (chord.tick + chord.duration > lastTickToConsider) + continue; + + if (chord.tick + chord.duration == lastTickToConsider && + chord.tick > deleteBeginTick) + { + deleteBeginTick = chord.tick; + } + else if (chord.tick + chord.duration != lastTickToConsider && + chord.tick + chord.duration > deleteBeginTick) + { + deleteBeginTick = chord.tick + chord.duration; + } + } + } + + if (considerNotes) + this.songData.removeNotesByTickRange(deleteBeginTick, lastTickToConsider, null); + + if (considerChords) + this.songData.removeChordsByTickRange(deleteBeginTick, lastTickToConsider); + + this.cursorTick = Math.max(0, deleteBeginTick); + this.clearHover(); + this.refreshRepresentation(); + this.refreshCanvas(); + this.callOnCursorChanged(); + } } \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js index 49c0419..450eaa8 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -77,37 +77,20 @@ SongData.prototype.isValidDuration = function(duration) } +// Returns whether the given pitch value is valid. +SongData.prototype.isValidPitch = function(pitch) +{ + return (pitch >= theory.getMinPitch() && pitch <= theory.getMaxPitch()) +} + + // Adds the given note to the data, and returns whether it was successful. SongData.prototype.addNote = function(note) { - if (!this.isValidTick(note.tick) || !this.isValidDuration(note.duration)) + if (!this.isValidTick(note.tick) || !this.isValidDuration(note.duration) || !this.isValidPitch(note.pitch)) return false; - // Clip notes which collide with the new one. - // TODO: Split a note into two in case the new one is contained within it. - for (var i = this.notes.length - 1; i >= 0; i--) - { - var otherNote = this.notes[i]; - - if (otherNote.pitch != note.pitch) - continue; - - if (otherNote.tick >= note.tick && otherNote.tick + otherNote.duration <= note.tick + note.duration) - { - this.notes.splice(i, 1); - } - else if (otherNote.tick >= note.tick && otherNote.tick < note.tick + note.duration && otherNote.tick + otherNote.duration > note.tick + note.duration) - { - var tickEnd = otherNote.tick + otherNote.duration; - otherNote.tick = note.tick + note.duration; - otherNote.duration = tickEnd - otherNote.tick; - } - else if (otherNote.tick < note.tick && otherNote.tick + otherNote.duration >= note.tick) - { - otherNote.duration = note.tick - otherNote.tick; - } - } - + this.removeNotesByTickRange(note.tick, note.tick + note.duration, note.pitch); arrayAddSortedByTick(this.notes, note); return true; } @@ -119,28 +102,7 @@ SongData.prototype.addChord = function(chord) if (!this.isValidTick(chord.tick) || !this.isValidDuration(chord.duration)) return false; - // Clip chords which collide with the new one. - // TODO: Split a chord into two in case the new one is contained within it. - for (var i = this.chords.length - 1; i >= 0; i--) - { - var otherChord = this.chords[i]; - - if (otherChord.tick >= chord.tick && otherChord.tick + otherChord.duration <= chord.tick + chord.duration) - { - this.chords.splice(i, 1); - } - else if (otherChord.tick >= chord.tick && otherChord.tick < chord.tick + chord.duration && otherChord.tick + otherChord.duration > chord.tick + chord.duration) - { - var tickEnd = otherChord.tick + otherChord.duration; - otherChord.tick = chord.tick + chord.duration; - otherChord.duration = tickEnd - otherChord.tick; - } - else if (otherChord.tick < chord.tick && otherChord.tick + otherChord.duration >= chord.tick) - { - otherChord.duration = chord.tick - otherChord.tick; - } - } - + this.removeChordsByTickRange(chord.tick, chord.tick + chord.duration); arrayAddSortedByTick(this.chords, chord); return true; } @@ -183,4 +145,55 @@ SongData.prototype.addMeterChange = function(meterChange) arrayAddSortedByTick(this.meterChanges, meterChange); return true; +} + + +SongData.prototype.removeNotesByTickRange = function(tickBegin, tickEnd, pitch) +{ + for (var i = this.notes.length - 1; i >= 0; i--) + { + var otherNote = this.notes[i]; + + if (pitch != null && otherNote.pitch != pitch) + continue; + + if (otherNote.tick >= tickBegin && otherNote.tick + otherNote.duration <= tickEnd) + { + this.notes.splice(i, 1); + } + else if (otherNote.tick >= tickBegin && otherNote.tick < tickEnd && otherNote.tick + otherNote.duration > tickEnd) + { + var otherNoteEndTick = otherNote.tick + otherNote.duration; + otherNote.tick = tickEnd; + otherNote.duration = otherNoteEndTick - otherNote.tick; + } + else if (otherNote.tick < tickBegin && otherNote.tick + otherNote.duration >= tickBegin) + { + otherNote.duration = tickBegin - otherNote.tick; + } + } +} + + +SongData.prototype.removeChordsByTickRange = function(tickBegin, tickEnd) +{ + for (var i = this.chords.length - 1; i >= 0; i--) + { + var otherChord = this.chords[i]; + + if (otherChord.tick >= tickBegin && otherChord.tick + otherChord.duration <= tickEnd) + { + this.chords.splice(i, 1); + } + else if (otherChord.tick >= tickBegin && otherChord.tick < tickEnd && otherChord.tick + otherChord.duration > tickEnd) + { + var otherNoteEndTick = otherChord.tick + otherChord.duration; + otherChord.tick = tickEnd; + otherChord.duration = otherNoteEndTick - otherChord.tick; + } + else if (otherChord.tick < tickBegin && otherChord.tick + otherChord.duration >= tickBegin) + { + otherChord.duration = tickBegin - otherChord.tick; + } + } } \ No newline at end of file diff --git a/src/theory.js b/src/theory.js index 76b8cf6..39205d9 100644 --- a/src/theory.js +++ b/src/theory.js @@ -87,13 +87,13 @@ function Theory() this.getMinPitch = function() { - return 60 - 12 * 3; + return 60 - 12 * 2; } this.getMaxPitch = function() { - return 60 + 12 * 3; + return 60 + 12 * 3 - 1; } diff --git a/src/toolbox.js b/src/toolbox.js index f894a5e..6686536 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -275,6 +275,7 @@ function Toolbox(div, editor, synth) that.editor.setInteractionEnabled(!that.playing); that.buttonPlay.innerHTML = (that.playing ? "■ Stop" : "▶ Play"); + that.editor.cursorTick = Math.floor(that.editor.cursorTick / that.editor.tickSnap) * that.editor.tickSnap; that.editor.showCursor = true; that.editor.unselectAll(); that.editor.clearHover(); @@ -651,7 +652,8 @@ Toolbox.prototype.processPlayback = function() for (var i = 0; i < this.editor.songData.chords.length; i++) { var chord = this.editor.songData.chords[i]; - if (chord.tick + chord.duration > lastCursorTick && chord.tick < this.editor.cursorTick) + if (chord.tick + chord.duration > lastCursorTick && chord.tick < this.editor.cursorTick && + !(chord.tick + chord.duration > lastCursorTick && chord.tick + chord.duration < this.editor.cursorTick)) { var halfTick = this.editor.WHOLE_NOTE_DURATION / 2; var quarterTick = halfTick / 2; From 218f830477e431674fc41c5013b244f1498dffa7 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 3 Dec 2015 08:39:16 -0200 Subject: [PATCH 16/46] prevent default action for delete/backspace --- src/editor_interaction.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 71e159d..9e40593 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -728,6 +728,8 @@ SongEditor.prototype.handleKeyDown = function(ev) if (keyCode == 46 || (keyCode == 8 && this.selectedObjects > 0)) // Delete/Backspace { + ev.preventDefault(); + // Remove selected objects from the song data. var selectedNotes = []; for (var n = this.noteSelections.length - 1; n >= 0; n--) @@ -778,6 +780,8 @@ SongEditor.prototype.handleKeyDown = function(ev) else if (keyCode == 8 && this.selectedObjects == 0) // Backspace { + ev.preventDefault(); + var lastTickToConsider = 0; var considerNotes = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_NOTES); var considerChords = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_CHORDS); From 994685a9cda79e95237831b7f486280cc8903687 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 3 Dec 2015 18:00:00 -0200 Subject: [PATCH 17/46] add save/load functionality --- src/songdata.js | 144 ++++++++++++++++++++++++++++++++++++++++++++---- src/theory.js | 24 ++++++++ src/toolbox.js | 53 +++++++++++++++++- 3 files changed, 210 insertions(+), 11 deletions(-) diff --git a/src/songdata.js b/src/songdata.js index 450eaa8..82acef1 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -1,12 +1,6 @@ function SongData() { - this.beatsPerMinute = 120; - this.ticksPerBeat = 960; - this.lastTick = 9600; - this.notes = []; - this.chords = []; - this.keyChanges = []; - this.meterChanges = []; + this.clear(); } @@ -61,11 +55,27 @@ function arrayAddSortedByTick(arr, obj) } +// Clears song data. +SongData.prototype.clear = function() +{ + this.beatsPerMinute = 120; + this.ticksPerBeat = 960; + this.lastTick = 9600; + this.notes = []; + this.chords = []; + this.keyChanges = []; + this.meterChanges = []; + + this.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); + this.addMeterChange(new SongDataMeterChange(0, 4, 4)); +} + + // Returns whether the given tick value is valid. SongData.prototype.isValidTick = function(tick) { // Arbitrary upper limit. - return (tick >= 0 && tick < 1000000); + return (tick >= 0 && tick < 1000000 && (tick % 1) == 0); } @@ -73,14 +83,14 @@ SongData.prototype.isValidTick = function(tick) SongData.prototype.isValidDuration = function(duration) { // Arbitrary upper limit. - return (duration > 0 && duration < 10000); + return (duration > 0 && duration < 10000 && (duration % 1) == 0); } // Returns whether the given pitch value is valid. SongData.prototype.isValidPitch = function(pitch) { - return (pitch >= theory.getMinPitch() && pitch <= theory.getMaxPitch()) + return (pitch >= theory.getMinPitch() && pitch <= theory.getMaxPitch() && (pitch % 1) == 0) } @@ -90,6 +100,7 @@ SongData.prototype.addNote = function(note) if (!this.isValidTick(note.tick) || !this.isValidDuration(note.duration) || !this.isValidPitch(note.pitch)) return false; + // Clip notes which were at the same range. this.removeNotesByTickRange(note.tick, note.tick + note.duration, note.pitch); arrayAddSortedByTick(this.notes, note); return true; @@ -102,6 +113,7 @@ SongData.prototype.addChord = function(chord) if (!this.isValidTick(chord.tick) || !this.isValidDuration(chord.duration)) return false; + // Clip chords which were at the same range. this.removeChordsByTickRange(chord.tick, chord.tick + chord.duration); arrayAddSortedByTick(this.chords, chord); return true; @@ -148,6 +160,8 @@ SongData.prototype.addMeterChange = function(meterChange) } +// Remove or truncate notes that fall between tickBegin and tickEnd, optionally only if +// their pitches match the given pitch. Pass null for pitch to not consider pitches. SongData.prototype.removeNotesByTickRange = function(tickBegin, tickEnd, pitch) { for (var i = this.notes.length - 1; i >= 0; i--) @@ -175,6 +189,7 @@ SongData.prototype.removeNotesByTickRange = function(tickBegin, tickEnd, pitch) } +// Remove or truncate chords that fall between tickBegin and tickEnd. SongData.prototype.removeChordsByTickRange = function(tickBegin, tickEnd) { for (var i = this.chords.length - 1; i >= 0; i--) @@ -196,4 +211,113 @@ SongData.prototype.removeChordsByTickRange = function(tickBegin, tickEnd) otherChord.duration = tickBegin - otherChord.tick; } } +} + + +// Returns a JSON string containing the song data. +SongData.prototype.save = function() +{ + var noteIndex = 0; + var chordIndex = 0; + var keyChangeIndex = 0; + var meterChangeIndex = 0; + + var json = "{\n"; + + json += " \"bpm\": " + this.beatsPerMinute + ",\n"; + json += " \"notes\": [\n"; + + for (var i = 0; i < this.notes.length; i++) + { + json += " "; + json += "[ " + this.notes[i].tick + ", "; + json += this.notes[i].duration + ", "; + json += this.notes[i].pitch + " ]"; + + if (i < this.notes.length - 1) + json += ","; + + json += "\n"; + } + + json += " ],\n \"chords\": [\n"; + + for (var i = 0; i < this.chords.length; i++) + { + json += " "; + json += "[ " + this.chords[i].tick + ", "; + json += this.chords[i].duration + ", "; + json += this.chords[i].rootPitch + ", "; + json += "{ \"chordId\": " + theory.getIdForChord(this.chords[i].chord) + " } ]"; + + if (i < this.chords.length - 1) + json += ","; + + json += "\n"; + } + + json += " ],\n \"keyChanges\": [\n"; + + for (var i = 0; i < this.keyChanges.length; i++) + { + json += " "; + json += "[ " + this.keyChanges[i].tick + ", "; + json += this.keyChanges[i].tonicPitch + ", "; + json += theory.getIdForScale(this.keyChanges[i].scale) + " ]"; + + if (i < this.keyChanges.length - 1) + json += ","; + + json += "\n"; + } + + json += " ],\n \"meterChanges\": [\n"; + + for (var i = 0; i < this.meterChanges.length; i++) + { + json += " "; + json += "[ " + this.meterChanges[i].tick + ", "; + json += this.meterChanges[i].numerator + ", "; + json += this.meterChanges[i].denominator + " ]"; + + if (i < this.meterChanges.length - 1) + json += ","; + + json += "\n"; + } + + json += " ]\n}"; + + return json; +} + + +// Loads the song data contained in the given string. +// Will throw an exception on error. +SongData.prototype.load = function(jsonStr) +{ + this.clear(); + + var song = JSON.parse(jsonStr); + this.beatsPerMinute = song.bpm; + + for (var i = 0; i < song.notes.length; i++) + { + this.addNote(new SongDataNote(song.notes[i][0], song.notes[i][1], song.notes[i][2])); + } + + for (var i = 0; i < song.chords.length; i++) + { + this.addChord(new SongDataChord(song.chords[i][0], song.chords[i][1], theory.getChordForId(song.chords[i][3].chordId), song.chords[i][2])); + } + + for (var i = 0; i < song.keyChanges.length; i++) + { + this.addKeyChange(new SongDataKeyChange(song.keyChanges[i][0], theory.getScaleForId(song.keyChanges[i][2]), song.keyChanges[i][1])); + } + + for (var i = 0; i < song.meterChanges.length; i++) + { + this.addMeterChange(new SongDataMeterChange(song.meterChanges[i][0], song.meterChanges[i][1], song.meterChanges[i][2])); + } } \ No newline at end of file diff --git a/src/theory.js b/src/theory.js index 39205d9..4a89142 100644 --- a/src/theory.js +++ b/src/theory.js @@ -112,6 +112,30 @@ function Theory() return numerals[(pitch + 12 - key.tonicPitch) % 12]; }; + + this.getIdForChord = function(chord) + { + return this.chords.indexOf(chord); + } + + + this.getChordForId = function(id) + { + return this.chords[id]; + } + + + this.getIdForScale = function(scale) + { + return this.scales.indexOf(scale); + } + + + this.getScaleForId = function(id) + { + return this.scales[id]; + } + this.getOctaveForPitch = function(pitch) { diff --git a/src/toolbox.js b/src/toolbox.js index 6686536..187fa2a 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -26,7 +26,7 @@ function Toolbox(div, editor, synth) this.mainLayoutCell01.appendChild(this.toolsLayout); - // Playback controls. + // Playback controls and global song settings. this.buttonPlay = document.createElement("button"); this.buttonPlay.innerHTML = "▶ Play"; this.buttonPlay.className = "toolboxButton"; @@ -50,6 +50,26 @@ function Toolbox(div, editor, synth) this.inputBPM.style.width = "30px"; this.mainLayoutCell00.appendChild(this.inputBPM); + this.mainLayoutCell00.appendChild(document.createElement("br")); + this.mainLayoutCell00.appendChild(document.createElement("br")); + + this.buttonSave = document.createElement("button"); + this.buttonSave.innerHTML = "Generate JSON"; + this.buttonSave.className = "toolboxButton"; + this.mainLayoutCell00.appendChild(this.buttonSave); + + this.buttonLoad = document.createElement("button"); + this.buttonLoad.innerHTML = "Load JSON"; + this.buttonLoad.className = "toolboxButton"; + this.mainLayoutCell00.appendChild(this.buttonLoad); + + this.mainLayoutCell00.appendChild(document.createElement("br")); + + this.inputSave = document.createElement("textarea"); + this.inputSave.style.width = "200px"; + this.inputSave.style.height = "100px"; + this.mainLayoutCell00.appendChild(this.inputSave); + // Key Change settings. this.keyChangeSpan = document.createElement("span"); this.keyChangeSpan.style.background = "#cccccc"; @@ -337,6 +357,37 @@ function Toolbox(div, editor, synth) that.inputBPM.value = that.editor.songData.beatsPerMinute; } + this.buttonSave.onclick = function() + { + that.inputSave.value = that.editor.songData.save(); + } + + this.buttonLoad.onclick = function() + { + if (window.confirm("Overwrite the current song?")) + { + try + { + that.editor.songData.load(that.inputSave.value); + } + catch (err) + { + window.alert("There was an error with the input:\n\n" + err) + that.editor.songData.clear(); + } + + that.editor.cursorTick = 0; + that.editor.showCursor = true; + that.editor.cursorZone = that.editor.CURSOR_ZONE_ALL; + that.editor.unselectAll(); + that.editor.clearHover(); + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.refresh(); + that.refreshSelection(); + } + } + this.refreshSelection(); this.refresh(); } From 2f49fe822f05d4fced79a753dd647139fd50c44a Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Fri, 4 Dec 2015 11:59:14 -0200 Subject: [PATCH 18/46] add horizontal scrolling --- src/editor.js | 5 +++-- src/editor_drawing.js | 25 ++++++++++++++++++++++--- src/editor_interaction.js | 6 ++++++ src/editor_representation.js | 2 +- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/editor.js b/src/editor.js index 8c8cd62..61c0279 100644 --- a/src/editor.js +++ b/src/editor.js @@ -40,6 +40,7 @@ function SongEditor(canvas, songData, synth) // These control scrolling. this.rowAtCenter = 8 * 5; + this.xAtLeft = 0; // These control cursor (the vertical blue bar) interaction. this.CURSOR_ZONE_ALL = 0; @@ -69,8 +70,8 @@ function SongEditor(canvas, songData, synth) // Layout constants. this.WHOLE_NOTE_DURATION = 960; - this.MARGIN_LEFT = 4; - this.MARGIN_RIGHT = 4; + this.MARGIN_LEFT = 16; + this.MARGIN_RIGHT = 16; this.MARGIN_TOP = 4; this.MARGIN_BOTTOM = 8; this.HEADER_MARGIN = 40; diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 02b97f3..2a5e3e2 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -17,6 +17,12 @@ SongEditor.prototype.refreshCanvas = function() for (var i = 0; i < this.viewBlocks.length; i++) { var block = this.viewBlocks[i]; + + // Don't draw bars out of the screen (with margin for between-block note parts). + if (block.x1 > this.canvasWidth || + block.x2 < -this.KEYCHANGE_BAR_WIDTH) + continue; + var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); var firstVisibleRowY = this.getYForRow(block, visibleRows.first); var lastVisibleRowY = this.getYForRow(block, visibleRows.last + 1); @@ -126,9 +132,8 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.strokeStyle = BLOCK_BORDER_COLOR; this.ctx.lineWidth = 2; - var x2 = Math.min(block.x2, this.canvasWidth - this.MARGIN_LEFT); - this.ctx.strokeRect(block.x1, block.y1, x2 - block.x1, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - block.y1); - this.ctx.strokeRect(block.x1, block.y2 - this.CHORD_HEIGHT, x2 - block.x1, this.CHORD_HEIGHT); + this.ctx.strokeRect(block.x1, block.y1, block.x2 - block.x1, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - block.y1); + this.ctx.strokeRect(block.x1, block.y2 - this.CHORD_HEIGHT, block.x2 - block.x1, this.CHORD_HEIGHT); // Draw cursor. if (this.showCursor && this.cursorTick >= block.tick && this.cursorTick < block.tick + block.duration) @@ -331,6 +336,20 @@ SongEditor.prototype.refreshCanvas = function() } + // Draw side fade-outs. + var leftFadeGradient = this.ctx.createLinearGradient(0, 0, this.MARGIN_LEFT, 0); + leftFadeGradient.addColorStop(0, "rgba(255, 255, 255, 1)"); + leftFadeGradient.addColorStop(1, "rgba(255, 255, 255, 0)"); + this.ctx.fillStyle = leftFadeGradient; + this.ctx.fillRect(0, 0, this.MARGIN_LEFT, this.canvasHeight); + + var rightFadeGradient = this.ctx.createLinearGradient(this.canvasWidth - this.MARGIN_RIGHT, 0, this.canvasWidth, 0); + rightFadeGradient.addColorStop(0, "rgba(255, 255, 255, 0)"); + rightFadeGradient.addColorStop(1, "rgba(255, 255, 255, 1)"); + this.ctx.fillStyle = rightFadeGradient; + this.ctx.fillRect(this.canvasWidth - this.MARGIN_RIGHT, 0, this.MARGIN_RIGHT, this.canvasHeight); + + this.ctx.restore(); } diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 9e40593..2765c87 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -464,6 +464,12 @@ SongEditor.prototype.handleMouseMove = function(ev) var rowLimits = this.getFirstAndLastScrollableRows(); this.rowAtCenter = Math.min(rowLimits.first, Math.max(rowLimits.last, this.rowAtCenter)); this.mouseDragOrigin.y = mousePos.y; + + var xOffset = (this.mouseDragOrigin.x - mousePos.x); + this.xAtLeft += xOffset; + this.xAtLeft = Math.max(0, Math.min(this.viewBlocks[this.viewBlocks.length - 1].x2 + this.canvasWidth / 2, this.xAtLeft)); + this.mouseDragOrigin.x = mousePos.x; + this.refreshRepresentation(); this.refreshCanvas(); } diff --git a/src/editor_representation.js b/src/editor_representation.js index 9453a29..af91b0c 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -37,7 +37,7 @@ SongEditor.prototype.refreshRepresentation = function() // Set up current block drawing position, current tick, and // iterators through the song data. - var x = this.MARGIN_LEFT; + var x = this.MARGIN_LEFT - this.xAtLeft; var tick = 0; var curNote = 0; var curChord = 0; From 3ffe98be9c09380dfc0185fd6df1a03b6944a578 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Fri, 4 Dec 2015 14:12:34 -0200 Subject: [PATCH 19/46] change toolbox layout to always show current key/meter; refactor toolbox table layout building to its own function --- src/editor_interaction.js | 28 +++- src/toolbox.js | 298 +++++++++++++++++++------------------- 2 files changed, 171 insertions(+), 155 deletions(-) diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 2765c87..113f341 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -147,19 +147,37 @@ SongEditor.prototype.getZoneAtPosition = function(y) } -SongEditor.prototype.getKeyAtTick = function(tick) +SongEditor.prototype.getBlockIndexAtTick = function(tick) { for (var b = 0; b < this.viewBlocks.length; b++) { var block = this.viewBlocks[b]; if (tick >= block.tick && tick < block.tick + block.duration) - { - return block.key; - } + return b; } - return new SongDataKeyChange(0, theory.scales[0], 0); + return -1; +} + + +SongEditor.prototype.getKeyAtTick = function(tick) +{ + var blockIndex = this.getBlockIndexAtTick(tick); + if (blockIndex >= 0) + return this.viewBlocks[blockIndex].key; + + return null; +} + + +SongEditor.prototype.getMeterAtTick = function(tick) +{ + var blockIndex = this.getBlockIndexAtTick(tick); + if (blockIndex >= 0) + return this.viewBlocks[blockIndex].meter; + + return null; } diff --git a/src/toolbox.js b/src/toolbox.js index 187fa2a..d0c771f 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -9,76 +9,65 @@ function Toolbox(div, editor, synth) this.initCSS(); - // Create elements. - this.mainLayout = document.createElement("table"); - this.mainLayout.style.margin = "auto"; - this.mainLayoutRow0 = document.createElement("tr"); this.mainLayout.appendChild(this.mainLayoutRow0); - this.mainLayoutCell00 = document.createElement("td"); this.mainLayoutRow0.appendChild(this.mainLayoutCell00); - this.mainLayoutCell01 = document.createElement("td"); this.mainLayoutRow0.appendChild(this.mainLayoutCell01); - this.div.appendChild(this.mainLayout); - - this.toolsLayout = document.createElement("table"); - this.toolsLayout.style.margin = "auto"; - this.toolsLayoutRow0 = document.createElement("tr"); this.toolsLayout.appendChild(this.toolsLayoutRow0); - this.toolsLayoutCell00 = document.createElement("td"); this.toolsLayoutRow0.appendChild(this.toolsLayoutCell00); - this.toolsLayoutRow1 = document.createElement("tr"); this.toolsLayout.appendChild(this.toolsLayoutRow1); - this.toolsLayoutCell10 = document.createElement("td"); this.toolsLayoutRow1.appendChild(this.toolsLayoutCell10); - this.mainLayoutCell01.appendChild(this.toolsLayout); - + // Main table layout. Working with divs is so terrible... + this.mainLayout = this.createTable(this.div, 2, 1); // Playback controls and global song settings. this.buttonPlay = document.createElement("button"); this.buttonPlay.innerHTML = "▶ Play"; this.buttonPlay.className = "toolboxButton"; this.buttonPlay.style.fontSize = "20px"; - this.mainLayoutCell00.appendChild(this.buttonPlay); + this.mainLayout.cells[0][0].appendChild(this.buttonPlay); this.buttonRewind = document.createElement("button"); this.buttonRewind.innerHTML = "◀◀ Rewind"; this.buttonRewind.className = "toolboxButton"; - this.mainLayoutCell00.appendChild(this.buttonRewind); + this.mainLayout.cells[0][0].appendChild(this.buttonRewind); - this.mainLayoutCell00.appendChild(document.createElement("br")); + this.mainLayout.cells[0][0].appendChild(document.createElement("br")); var inputBPMSpan = document.createElement("span"); inputBPMSpan.innerHTML = "Tempo "; inputBPMSpan.className = "toolboxText"; - this.mainLayoutCell00.appendChild(inputBPMSpan); + this.mainLayout.cells[0][0].appendChild(inputBPMSpan); this.inputBPM = document.createElement("input"); this.inputBPM.value = editor.songData.beatsPerMinute; this.inputBPM.style.width = "30px"; - this.mainLayoutCell00.appendChild(this.inputBPM); + this.mainLayout.cells[0][0].appendChild(this.inputBPM); - this.mainLayoutCell00.appendChild(document.createElement("br")); - this.mainLayoutCell00.appendChild(document.createElement("br")); + this.mainLayout.cells[0][0].appendChild(document.createElement("br")); + this.mainLayout.cells[0][0].appendChild(document.createElement("br")); this.buttonSave = document.createElement("button"); this.buttonSave.innerHTML = "Generate JSON"; this.buttonSave.className = "toolboxButton"; - this.mainLayoutCell00.appendChild(this.buttonSave); + this.mainLayout.cells[0][0].appendChild(this.buttonSave); this.buttonLoad = document.createElement("button"); this.buttonLoad.innerHTML = "Load JSON"; this.buttonLoad.className = "toolboxButton"; - this.mainLayoutCell00.appendChild(this.buttonLoad); + this.mainLayout.cells[0][0].appendChild(this.buttonLoad); - this.mainLayoutCell00.appendChild(document.createElement("br")); + this.mainLayout.cells[0][0].appendChild(document.createElement("br")); this.inputSave = document.createElement("textarea"); this.inputSave.style.width = "200px"; this.inputSave.style.height = "100px"; - this.mainLayoutCell00.appendChild(this.inputSave); + this.mainLayout.cells[0][0].appendChild(this.inputSave); + + // Key/Meter table layout. + this.keyMeterLayout = this.createTable(this.mainLayout.cells[0][1], 2, 1); // Key Change settings. - this.keyChangeSpan = document.createElement("span"); - this.keyChangeSpan.style.background = "#cccccc"; + this.keyMeterLayout.cells[0][0].style.background = "#cccccc"; + this.keyMeterLayout.cells[0][0].style.padding = "10px"; var keyChangeEditingLabelSpan = document.createElement("span"); - keyChangeEditingLabelSpan.innerHTML = "Edit Key Change"; + keyChangeEditingLabelSpan.innerHTML = "Key"; keyChangeEditingLabelSpan.className = "toolboxLabel"; - this.keyChangeSpan.appendChild(keyChangeEditingLabelSpan); - this.keyChangeSpan.appendChild(document.createElement("br")); + this.keyMeterLayout.cells[0][0].appendChild(keyChangeEditingLabelSpan); + this.keyMeterLayout.cells[0][0].appendChild(document.createElement("br")); this.keyChangeTonicSelect = document.createElement("select"); this.keyChangeTonicOptions = []; @@ -87,12 +76,12 @@ function Toolbox(div, editor, synth) this.keyChangeTonicOptions[i] = document.createElement("option"); this.keyChangeTonicSelect.appendChild(this.keyChangeTonicOptions[i]); } - this.keyChangeSpan.appendChild(this.keyChangeTonicSelect); + this.keyMeterLayout.cells[0][0].appendChild(this.keyChangeTonicSelect); var keyChangeDividerSpan = document.createElement("span"); keyChangeDividerSpan.innerHTML = " "; keyChangeDividerSpan.className = "toolboxText"; - this.keyChangeSpan.appendChild(keyChangeDividerSpan); + this.keyMeterLayout.cells[0][0].appendChild(keyChangeDividerSpan); this.keyChangeScaleSelect = document.createElement("select"); this.keyChangeScaleOptions = []; @@ -102,27 +91,30 @@ function Toolbox(div, editor, synth) this.keyChangeScaleOptions[i].innerHTML = theory.scales[i].name; this.keyChangeScaleSelect.appendChild(this.keyChangeScaleOptions[i]); } - this.keyChangeSpan.appendChild(this.keyChangeScaleSelect); + this.keyMeterLayout.cells[0][0].appendChild(this.keyChangeScaleSelect); this.buttonKeyChangeListen = document.createElement("button"); this.buttonKeyChangeListen.innerHTML = "Listen"; this.buttonKeyChangeListen.className = "toolboxButton"; - this.keyChangeSpan.appendChild(this.buttonKeyChangeListen); + this.keyMeterLayout.cells[0][0].appendChild(this.buttonKeyChangeListen); - this.keyChangeSpan.appendChild(document.createElement("br")); - this.keyChangeSpan.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.keyChangeSpan); + this.keyMeterLayout.cells[0][0].appendChild(document.createElement("br")); + + this.buttonAddKeyChange = document.createElement("button"); + this.buttonAddKeyChange.innerHTML = "Add Key Change"; + this.buttonAddKeyChange.className = "toolboxButton"; + this.keyMeterLayout.cells[0][0].appendChild(this.buttonAddKeyChange); // Meter Change settings. - this.meterChangeSpan = document.createElement("span"); - this.meterChangeSpan.style.background = "#aaddff"; + this.keyMeterLayout.cells[0][1].style.background = "#aaddff"; + this.keyMeterLayout.cells[0][1].style.padding = "10px"; var meterChangeEditingLabelSpan = document.createElement("span"); - meterChangeEditingLabelSpan.innerHTML = "Edit Meter Change"; + meterChangeEditingLabelSpan.innerHTML = "Meter"; meterChangeEditingLabelSpan.className = "toolboxLabel"; - this.meterChangeSpan.appendChild(meterChangeEditingLabelSpan); - this.meterChangeSpan.appendChild(document.createElement("br")); + this.keyMeterLayout.cells[0][1].appendChild(meterChangeEditingLabelSpan); + this.keyMeterLayout.cells[0][1].appendChild(document.createElement("br")); this.meterChangeNumeratorSelect = document.createElement("select"); this.meterChangeNumeratorOptions = []; @@ -132,12 +124,12 @@ function Toolbox(div, editor, synth) this.meterChangeNumeratorOptions[i].innerHTML = "" + (i + 1); this.meterChangeNumeratorSelect.appendChild(this.meterChangeNumeratorOptions[i]); } - this.meterChangeSpan.appendChild(this.meterChangeNumeratorSelect); + this.keyMeterLayout.cells[0][1].appendChild(this.meterChangeNumeratorSelect); var meterChangeDividerSpan = document.createElement("span"); meterChangeDividerSpan.innerHTML = " / "; meterChangeDividerSpan.className = "toolboxText"; - this.meterChangeSpan.appendChild(meterChangeDividerSpan); + this.keyMeterLayout.cells[0][1].appendChild(meterChangeDividerSpan); this.meterChangeDenominatorSelect = document.createElement("select"); this.meterChangeDenominatorOptions = []; @@ -148,36 +140,14 @@ function Toolbox(div, editor, synth) this.meterChangeDenominatorOptions[i].innerHTML = "" + this.meterChangeDenominators[i]; this.meterChangeDenominatorSelect.appendChild(this.meterChangeDenominatorOptions[i]); } - this.meterChangeSpan.appendChild(this.meterChangeDenominatorSelect); - - this.meterChangeSpan.appendChild(document.createElement("br")); - this.meterChangeSpan.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.meterChangeSpan); - + this.keyMeterLayout.cells[0][1].appendChild(this.meterChangeDenominatorSelect); - // Editing controls. - this.labelKey = document.createElement("span"); - this.labelKey.className = "toolboxLabel"; - this.labelKey.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.labelKey); - - - // Add Key/Meter Changes buttons. - this.changesSpan = document.createElement("span"); - - this.buttonAddKeyChange = document.createElement("button"); - this.buttonAddKeyChange.innerHTML = "Add Key Change"; - this.buttonAddKeyChange.className = "toolboxButton"; - this.changesSpan.appendChild(this.buttonAddKeyChange); + this.keyMeterLayout.cells[0][1].appendChild(document.createElement("br")); this.buttonAddMeterChange = document.createElement("button"); this.buttonAddMeterChange.innerHTML = "Add Meter Change"; this.buttonAddMeterChange.className = "toolboxButton"; - this.changesSpan.appendChild(this.buttonAddMeterChange); - - this.changesSpan.appendChild(document.createElement("br")); - this.changesSpan.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.changesSpan); + this.keyMeterLayout.cells[0][1].appendChild(this.buttonAddMeterChange); // Note buttons. @@ -193,7 +163,7 @@ function Toolbox(div, editor, synth) this.notesSpan.appendChild(document.createElement("br")); this.notesSpan.appendChild(document.createElement("br")); - this.toolsLayoutCell00.appendChild(this.notesSpan); + this.mainLayout.cells[0][1].appendChild(this.notesSpan); // Chord buttons. @@ -235,7 +205,7 @@ function Toolbox(div, editor, synth) this.chordsSpan.appendChild(document.createElement("br")); } - this.toolsLayoutCell00.appendChild(this.chordsSpan); + this.mainLayout.cells[0][1].appendChild(this.chordsSpan); // Set up callbacks. @@ -244,7 +214,7 @@ function Toolbox(div, editor, synth) var that = this; editor.addOnCursorChanged(function() { that.refresh(); }); - editor.addOnSelectionChanged(function() { that.refreshSelection(); }); + editor.addOnSelectionChanged(function() { that.refresh(); }); this.chordSelect.onchange = function() { that.refresh(); }; this.keyChangeTonicSelect.onchange = function() { that.editKeyChange(); }; this.keyChangeScaleSelect.onchange = function() { that.editKeyChange(); }; @@ -270,7 +240,6 @@ function Toolbox(div, editor, synth) that.editor.selectKeyChange(keyChange); that.editor.refreshCanvas(); that.refresh(); - that.refreshSelection(); }; this.buttonAddMeterChange.onclick = function(ev) @@ -286,7 +255,6 @@ function Toolbox(div, editor, synth) that.editor.selectMeterChange(meterChange); that.editor.refreshCanvas(); that.refresh(); - that.refreshSelection(); }; this.buttonPlay.onclick = function() @@ -302,7 +270,6 @@ function Toolbox(div, editor, synth) that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); - that.refreshSelection(); that.synth.stopAll(); if (that.playing) @@ -337,7 +304,6 @@ function Toolbox(div, editor, synth) that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); - that.refreshSelection(); } this.inputBPM.onchange = function() @@ -384,11 +350,9 @@ function Toolbox(div, editor, synth) that.editor.refreshRepresentation(); that.editor.refreshCanvas(); that.refresh(); - that.refreshSelection(); } } - this.refreshSelection(); this.refresh(); } @@ -423,19 +387,95 @@ Toolbox.prototype.initCSS = function() } +Toolbox.prototype.createTable = function(parent, columns, rows) +{ + var table = {}; + table.root = document.createElement("table"); + table.root.style.margin = "auto"; + + table.rows = []; + table.cells = []; + + for (var j = 0; j < rows; j++) + { + table.cells[j] = []; + + table.rows[j] = document.createElement("tr"); + table.root.appendChild(table.rows[j]); + + for (var i = 0; i < columns; i++) + { + table.cells[j][i] = document.createElement("td"); + table.rows[j].appendChild(table.cells[j][i]); + } + } + + parent.appendChild(table.root); + return table; +} + + Toolbox.prototype.refresh = function() { - var key = this.editor.getKeyAtTick(this.editor.cursorTick); + if (this.playing) + return; + + + this.keyMeterLayout.cells[0][0].style.visibility = "hidden"; + this.keyMeterLayout.cells[0][1].style.visibility = "hidden"; + + + // Refresh key change tool. + var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); + if (keyChange != null) + { + var scaleIndex = 0; + for (var i = 0; i < theory.scales.length; i++) + { + if (keyChange.scale == theory.scales[i]) + { + scaleIndex = i; + break; + } + } + + this.keyChangeScaleSelect.selectedIndex = scaleIndex; + + for (var i = 0; i < 12; i++) + { + var pitch = i; + this.keyChangeTonicOptions[i].innerHTML = theory.getNameForPitch(pitch, keyChange); + } + + this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; + this.keyMeterLayout.cells[0][0].style.visibility = "visible"; + } + + + // Refresh meter change tool. + var meterChange = this.editor.getMeterAtTick(this.editor.cursorTick); + if (meterChange != null) + { + this.meterChangeNumeratorSelect.selectedIndex = meterChange.numerator - 1; + if (meterChange.denominator == 2) this.meterChangeDenominatorSelect.selectedIndex = 0; + else if (meterChange.denominator == 4) this.meterChangeDenominatorSelect.selectedIndex = 1; + else if (meterChange.denominator == 8) this.meterChangeDenominatorSelect.selectedIndex = 2; + else if (meterChange.denominator == 16) this.meterChangeDenominatorSelect.selectedIndex = 3; + + this.keyMeterLayout.cells[0][1].style.visibility = "visible"; + } + - this.labelKey.innerHTML = "Key of " + theory.getNameForPitch(key.tonicPitch, key.scale) + " " + key.scale.name; - this.labelKey.appendChild(document.createElement("br")); + // Refresh note tools. + if (keyChange == null) + keyChange = new SongDataKeyChange(0, theory.scales[0], theory.C); for (var i = 0; i < 12; i++) { - var pitch = (i + key.tonicPitch) % 12; - this.buttonNotes[i].innerHTML = theory.getNameForPitch(pitch, key.scale); + var pitch = (i + keyChange.tonicPitch) % 12; + this.buttonNotes[i].innerHTML = theory.getNameForPitch(pitch, keyChange.scale); - var degree = theory.getDegreeForPitch(pitch, key); + var degree = theory.getDegreeForPitch(pitch, keyChange); if ((degree % 1) == 0) { @@ -473,25 +513,28 @@ Toolbox.prototype.refresh = function() }(pitch + 60); } + + // Refresh chord tools. var chordCategory = this.chordSelect.selectedIndex; + // "In Key" category. if (chordCategory == 0) { for (var i = 0; i < 7; i++) { - var pitch1 = key.tonicPitch + key.scale.degrees[i]; + var pitch1 = keyChange.tonicPitch + keyChange.scale.degrees[i]; - var pitch2 = key.tonicPitch + key.scale.degrees[(i + 2) % key.scale.degrees.length]; - if ((i + 2) >= key.scale.degrees.length) + var pitch2 = keyChange.tonicPitch + keyChange.scale.degrees[(i + 2) % keyChange.scale.degrees.length]; + if ((i + 2) >= keyChange.scale.degrees.length) pitch2 += 12; - var pitch3 = key.tonicPitch + key.scale.degrees[(i + 4) % key.scale.degrees.length]; - if ((i + 4) >= key.scale.degrees.length) + var pitch3 = keyChange.tonicPitch + keyChange.scale.degrees[(i + 4) % keyChange.scale.degrees.length]; + if ((i + 4) >= keyChange.scale.degrees.length) pitch3 += 12; var color = theory.getColorForDegree(i); var chord = theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); - var numeral = theory.getRomanNumeralForPitch(pitch1, key); + var numeral = theory.getRomanNumeralForPitch(pitch1, keyChange); var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); romanText += "" + chord.romanSup + ""; @@ -529,6 +572,8 @@ Toolbox.prototype.refresh = function() for (var i = 7; i < 12; i++) this.buttonChords[i].style.display = "none"; } + + // Other chord categories. else { var inKeyIndex = 0; @@ -536,8 +581,8 @@ Toolbox.prototype.refresh = function() for (var i = 0; i < 12; i++) { - var rootPitch = (key.tonicPitch + i) % 12; - var degree = theory.getDegreeForPitch(rootPitch, key); + var rootPitch = (keyChange.tonicPitch + i) % 12; + var degree = theory.getDegreeForPitch(rootPitch, keyChange); var index = inKeyIndex; if ((degree % 1) != 0) @@ -545,7 +590,7 @@ Toolbox.prototype.refresh = function() var color = theory.getColorForDegree(degree); var chord = theory.chords[chordCategory - 1]; - var numeral = theory.getRomanNumeralForPitch(rootPitch, key); + var numeral = theory.getRomanNumeralForPitch(rootPitch, keyChange); var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); romanText += "" + chord.romanSup + ""; @@ -597,65 +642,15 @@ Toolbox.prototype.refresh = function() } -Toolbox.prototype.refreshSelection = function() -{ - this.labelKey.style.display = "inline"; - this.changesSpan.style.display = "inline"; - this.notesSpan.style.display = "inline"; - this.chordsSpan.style.display = "inline"; - this.keyChangeSpan.style.display = "none"; - this.meterChangeSpan.style.display = "none"; - - - var keyChange = this.editor.getUniqueKeyChangeSelected(); - if (keyChange != null) - { - var scaleIndex = 0; - for (var i = 0; i < theory.scales.length; i++) - { - if (keyChange.scale == theory.scales[i]) - { - scaleIndex = i; - break; - } - } - - this.keyChangeScaleSelect.selectedIndex = scaleIndex; - - for (var i = 0; i < 12; i++) - { - var pitch = i; - this.keyChangeTonicOptions[i].innerHTML = theory.getNameForPitch(pitch, keyChange); - } - - this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; - - this.keyChangeSpan.style.display = "block"; - return; - } - - - var meterChange = this.editor.getUniqueMeterChangeSelected(); - if (meterChange != null) - { - this.meterChangeNumeratorSelect.selectedIndex = meterChange.numerator - 1; - if (meterChange.denominator == 2) this.meterChangeDenominatorSelect.selectedIndex = 0; - else if (meterChange.denominator == 4) this.meterChangeDenominatorSelect.selectedIndex = 1; - else if (meterChange.denominator == 8) this.meterChangeDenominatorSelect.selectedIndex = 2; - else if (meterChange.denominator == 16) this.meterChangeDenominatorSelect.selectedIndex = 3; - - this.meterChangeSpan.style.display = "block"; - return; - } -} - - Toolbox.prototype.editKeyChange = function() { if (this.playing) return; - var keyChange = this.editor.getUniqueKeyChangeSelected(); + var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); + if (keyChange == null) + return; + keyChange.scale = theory.scales[this.keyChangeScaleSelect.selectedIndex]; keyChange.tonicPitch = this.keyChangeTonicSelect.selectedIndex; @@ -671,7 +666,10 @@ Toolbox.prototype.editMeterChange = function() if (this.playing) return; - var meterChange = this.editor.getUniqueMeterChangeSelected(); + var meterChange = this.editor.getMeterAtTick(this.editor.cursorTick); + if (meterChange == null) + return; + meterChange.numerator = this.meterChangeNumeratorSelect.selectedIndex + 1; meterChange.denominator = this.meterChangeDenominators[this.meterChangeDenominatorSelect.selectedIndex]; From 5a0ed77315738c68bf9b7a54798102da6e72e9dd Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 5 Dec 2015 16:53:29 -0200 Subject: [PATCH 20/46] fix scale listen button --- src/toolbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/toolbox.js b/src/toolbox.js index d0c771f..a1860ef 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -223,7 +223,7 @@ function Toolbox(div, editor, synth) this.buttonKeyChangeListen.onclick = function() { - var keyChange = that.editor.getUniqueKeyChangeSelected(); + var keyChange = that.editor.getKeyAtTick(that.editor.cursorTick); theory.playScaleSample(that.synth, keyChange.scale, keyChange.tonicPitch); } From 1339d7bb28dab8303ba3e8895c1b24a4da975daa Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 9 Jan 2016 20:32:03 -0200 Subject: [PATCH 21/46] simplify pitch-row translation; split theory functions that take keys into taking scales and tonic pitches --- src/editor_drawing.js | 22 ++-- src/editor_representation.js | 4 +- src/theory.js | 197 ++++++++++++++++------------------- src/toolbox.js | 22 ++-- 4 files changed, 115 insertions(+), 130 deletions(-) diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 2a5e3e2..e30c632 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -38,7 +38,7 @@ SongEditor.prototype.refreshCanvas = function() // Draw rows. for (var row = visibleRows.first; row <= visibleRows.last + 1; row++) { - this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key) % 12 == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); + this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key.scale, block.key.tonicPitch) % 12 == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(block.x1, this.getYForRow(block, row)); @@ -239,7 +239,7 @@ SongEditor.prototype.refreshCanvas = function() var songKeyChange = this.songData.keyChanges[keyChange.keyChangeIndex]; this.ctx.fillStyle = KEY_BORDER_COLOR; this.ctx.fillText( - "" + theory.getNameForPitch(songKeyChange.tonicPitch, songKeyChange.scale) + " " + songKeyChange.scale.name, + "" + theory.getNameForPitch(songKeyChange.tonicPitch, songKeyChange.scale, songKeyChange.tonicPitch) + " " + songKeyChange.scale.name, textX, keyChange.y1, this.viewBlocks[keyChange.blockIndex].x2 - keyChange.x1 - 32); @@ -250,12 +250,12 @@ SongEditor.prototype.refreshCanvas = function() var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); for (var row = visibleRows.first; row <= visibleRows.last; row++) { - var pitch = theory.getPitchForRow(row, songKeyChange); + var pitch = theory.getPitchForRow(row, songKeyChange.scale, songKeyChange.tonicPitch); var y = this.getYForRow(this.viewBlocks[keyChange.blockIndex], row + 1); this.ctx.fillStyle = KEY_PITCH_COLOR; this.ctx.fillText( - "" + theory.getNameForPitch(pitch), + "" + theory.getNameForPitch(pitch, songKeyChange.scale, songKeyChange.tonicPitch), keyChange.x1 + 4, y); @@ -365,8 +365,8 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove var scrollY = -this.rowAtCenter * this.NOTE_HEIGHT; - var deg = theory.getDegreeForPitch(pitch, block.key); - var row = theory.getRowForPitch(pitch, block.key); + var deg = theory.getDegreeForPitch(pitch, block.key.scale, block.key.tonicPitch); + var row = theory.getRowForPitch(pitch, block.key.scale, block.key.tonicPitch); var pos = this.getNotePosition(block, row, tick, duration); this.drawDegreeColoredRectangle(deg, pos); @@ -394,7 +394,7 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) { var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = theory.getRowForPitch(pitch, nextBlock.key); + var nextRow = theory.getRowForPitch(pitch, nextBlock.key.scale, nextBlock.key.tonicPitch); var nextPos = this.getNotePosition(block, nextRow, tick, duration); var col = theory.getColorForDegree(deg - (deg % 1)); @@ -422,14 +422,14 @@ SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hov tick >= block.tick + block.duration) return; - var deg = theory.getDegreeForPitch(chord.rootPitch, block.key); + var deg = theory.getDegreeForPitch(chord.rootPitch, block.key.scale, block.key.tonicPitch); var pos = this.getChordPosition(block, tick, duration); this.drawDegreeColoredRectangle(deg, { x1: pos.x1, y1: pos.y1, x2: pos.x2, y2: pos.y1 + this.CHORD_ORNAMENT_HEIGHT }); this.drawDegreeColoredRectangle(deg, { x1: pos.x1, y1: pos.y2 - this.CHORD_ORNAMENT_HEIGHT, x2: pos.x2, y2: pos.y2 }); // Draw roman symbol. - var numeral = theory.getRomanNumeralForPitch(chord.rootPitch, block.key); + var numeral = theory.getRomanNumeralForPitch(chord.rootPitch, block.key.scale, block.key.tonicPitch); var romanText = chord.chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); this.ctx.fillStyle = "#000000"; @@ -537,7 +537,7 @@ SongEditor.prototype.getFirstAndLastVisibleRowsForBlock = function(block) var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; return { - first: Math.max(Math.floor(this.rowAtCenter - noteAreaHeight / 2 / this.NOTE_HEIGHT), theory.getRowForPitch(theory.getMinPitch(), block.key)), - last: Math.min(Math.ceil(this.rowAtCenter + noteAreaHeight / 2 / this.NOTE_HEIGHT), theory.getRowForPitch(theory.getMaxPitch(), block.key)) + first: Math.max(Math.floor(this.rowAtCenter - noteAreaHeight / 2 / this.NOTE_HEIGHT), 7 * 3), + last: Math.min(Math.ceil(this.rowAtCenter + noteAreaHeight / 2 / this.NOTE_HEIGHT), 7 * 8 - 1) }; } \ No newline at end of file diff --git a/src/editor_representation.js b/src/editor_representation.js index af91b0c..f5ebc1f 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -155,7 +155,7 @@ SongEditor.prototype.refreshRepresentation = function() if (note.tick + note.duration <= block.tick || note.tick >= block.tick + block.duration) continue; - var noteRow = theory.getRowForPitch(note.pitch, block.key); + var noteRow = theory.getRowForPitch(note.pitch, block.key.scale, block.key.tonicPitch); var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); block.notes.push( { @@ -237,7 +237,7 @@ SongEditor.prototype.getNotePosition = function(block, row, tick, duration) { var blockTick = tick - block.tick; var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; - var firstRow = block.key.scale.degrees.length * 3; + var firstRow = 7 * 3; var rowY = this.getYForRow(block, row); return { resizeHandleL: block.x1 + blockTick * this.tickZoom, diff --git a/src/theory.js b/src/theory.js index 4a89142..960f81b 100644 --- a/src/theory.js +++ b/src/theory.js @@ -30,58 +30,81 @@ function Theory() this.As = As; this.B = B; - + + // Scale without pitches will be generated below. this.scales = [ - { name: "Major", degrees: [ C, D, E, F, G, A, B ] }, - { name: "Dorian", degrees: null }, - { name: "Phrygian", degrees: null }, - { name: "Lydian", degrees: null }, - { name: "Mixolydian", degrees: null }, - { name: "Natural Minor", degrees: null }, - { name: "Locrian", degrees: null }, - { name: "Harmonic Minor", degrees: [ C, D, Ds, F, G, Gs, B ] }, - { name: "Double Harmonic", degrees: [ C, Cs, E, F, G, Gs, B ] }, - { name: "Lydian ♯2 ♯6", degrees: null }, - { name: "Ultraphrygian", degrees: null }, - { name: "Hungarian Minor", degrees: null }, - { name: "Oriental", degrees: null }, - { name: "Ionian Augmented ♯2", degrees: null }, - { name: "Locrian ♭♭3 ♭♭7", degrees: null }, - { name: "Phrygian Dominant", degrees: [ C, Cs, E, F, G, Gs, As ] }, + { name: "Major", pitches: [ C, D, E, F, G, A, B ] }, + { name: "Dorian" }, + { name: "Phrygian" }, + { name: "Lydian" }, + { name: "Mixolydian" }, + { name: "Natural Minor" }, + { name: "Locrian" }, + + { name: "Harmonic Minor", pitches: [ C, D, Ds, F, G, Gs, B ] }, + + { name: "Double Harmonic", pitches: [ C, Cs, E, F, G, Gs, B ] }, + { name: "Lydian ♯2 ♯6" }, + { name: "Ultraphrygian" }, + { name: "Hungarian Minor" }, + { name: "Oriental" }, + { name: "Ionian Augmented ♯2" }, + { name: "Locrian ♭♭3 ♭♭7" }, + + { name: "Phrygian Dominant", pitches: [ C, Cs, E, F, G, Gs, As ] }, ]; - // Generate the other modes of the base scales. + // Generate the other modes of the non-null scales. for (var i = 0; i < this.scales.length; i++) { - if (this.scales[i].degrees == null) + if (!this.scales[i].hasOwnProperty("pitches")) { - this.scales[i].degrees = []; - var offset = this.scales[i - 1].degrees[1]; + this.scales[i].pitches = []; + var offset = this.scales[i - 1].pitches[1]; for (var j = 0; j < 7; j++) { - this.scales[i].degrees[j] = (this.scales[i - 1].degrees[(j + 1) % 7] + 12 - offset) % 12; + this.scales[i].pitches[j] = (this.scales[i - 1].pitches[(j + 1) % 7] + 12 - offset) % 12; } } } + // Generate the mapping from pitch to scale degree. + for (var i = 0; i < this.scales.length; i++) + { + this.scales[i].pitchToDegreeMap = []; + + var curPitch = 0; + for (var j = 0; j < 7; j++) + { + while (curPitch < this.scales[i].pitches[j]) + { + this.scales[i].pitchToDegreeMap.push(j - 0.5); + curPitch++; + } + + this.scales[i].pitchToDegreeMap.push(j); + curPitch++; + } + } + this.chords = [ - { name: "Major", roman: "X", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, - { name: "Minor", roman: "x", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, - { name: "Diminished", roman: "x", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, - { name: "Augmented", roman: "X", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] }, - { name: "Flat Fifth(?)", roman: "X", romanSup: "(♭5)", romanSub: "", pitches: [ C, E, Fs ] }, - { name: "?", roman: "X", romanSup: "?", romanSub: "", pitches: [ C, D, Fs ] }, - { name: "Dominant Seventh", roman: "X", romanSup: "7", romanSub: "", pitches: [ C, E, G, As ] }, - { name: "Major Seventh", roman: "X", romanSup: "M7", romanSub: "", pitches: [ C, E, G, B ] }, - { name: "Minor Seventh", roman: "x", romanSup: "7", romanSub: "", pitches: [ C, Ds, G, As ] }, - { name: "Minor-Major Seventh", roman: "X", romanSup: "m(M7)", romanSub: "", pitches: [ C, Ds, G, B ] }, - { name: "Diminished Seventh", roman: "x", romanSup: "o7", romanSub: "", pitches: [ C, Ds, G, A ] }, - { name: "Half-Diminished Seventh", roman: "x", romanSup: "ø7", romanSub: "", pitches: [ C, Ds, Fs, As ] }, - { name: "Augmented Seventh", roman: "X", romanSup: "+7", romanSub: "", pitches: [ C, E, Gs, As ] }, - { name: "Augmented Major Seventh", roman: "X", romanSup: "+(M7)", romanSub: "", pitches: [ C, E, Gs, B ] } + { name: "Major", roman: "X", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, + { name: "Minor", roman: "x", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, + { name: "Diminished", roman: "x", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, + { name: "Augmented", roman: "X", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] }, + { name: "Flat Fifth(?)", roman: "X", romanSup: "(♭5)", romanSub: "", pitches: [ C, E, Fs ] }, + { name: "?", roman: "X", romanSup: "?", romanSub: "", pitches: [ C, D, Fs ] }, + { name: "Dominant Seventh", roman: "X", romanSup: "7", romanSub: "", pitches: [ C, E, G, As ] }, + { name: "Major Seventh", roman: "X", romanSup: "M7", romanSub: "", pitches: [ C, E, G, B ] }, + { name: "Minor Seventh", roman: "x", romanSup: "7", romanSub: "", pitches: [ C, Ds, G, As ] }, + { name: "Minor-Major Seventh", roman: "X", romanSup: "m(M7)", romanSub: "", pitches: [ C, Ds, G, B ] }, + { name: "Diminished Seventh", roman: "x", romanSup: "o7", romanSub: "", pitches: [ C, Ds, G, A ] }, + { name: "Half-Diminished Seventh", roman: "x", romanSup: "ø7", romanSub: "", pitches: [ C, Ds, Fs, As ] }, + { name: "Augmented Seventh", roman: "X", romanSup: "+7", romanSub: "", pitches: [ C, E, Gs, As ] }, + { name: "Augmented Major Seventh", roman: "X", romanSup: "+(M7)", romanSub: "", pitches: [ C, E, Gs, B ] } ]; @@ -97,7 +120,7 @@ function Theory() } - this.getNameForPitch = function(pitch, scale) + this.getNameForPitch = function(pitch, scale, tonicPitch) { // TODO: Take the scale also into consideration. var notes = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; @@ -105,11 +128,11 @@ function Theory() }; - this.getRomanNumeralForPitch = function(pitch, key) + this.getRomanNumeralForPitch = function(pitch, scale, tonicPitch) { // TODO: Take the scale also into consideration for naming. var numerals = ["I", "♭II", "II", "♭III", "III", "IV", "♭V", "V", "♭VI", "VI", "♭VII", "VII"]; - return numerals[(pitch + 12 - key.tonicPitch) % 12]; + return numerals[(pitch + 12 - tonicPitch) % 12]; }; @@ -143,24 +166,25 @@ function Theory() }; - this.getOctaveForDegree = function(degree, key) + this.getOctaveForDegree = function(degree) { - return Math.floor(degree / key.scale.degrees.length); + return Math.floor(degree / 7); }; + var colors = + [ + "#ff0000", + "#ffb014", + "#efe600", + "#00d300", + "#4800ff", + "#b800e5", + "#ff00cb" + ]; + this.getColorForDegree = function(degree) { - var colors = - [ - "#ff0000", - "#ffb014", - "#efe600", - "#00d300", - "#4800ff", - "#b800e5", - "#ff00cb" - ]; return colors[degree]; }; @@ -168,78 +192,39 @@ function Theory() // Returns the scale degree of the given pitch, according to the given key. // May return fractional values, which indicates that the pitch falls // between scale degrees. - this.getDegreeForPitch = function(pitch, key) + this.getDegreeForPitch = function(pitch, scale, tonicPitch) { - var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); - var pitchDegree = key.scale.degrees.length - 0.5; - - for (var i = 0; i < key.scale.degrees.length; i++) - { - if (key.scale.degrees[i] == pitchInOctave) - { - pitchDegree = i; - break; - } - else if (key.scale.degrees[i] > pitchInOctave) - { - pitchDegree = i - 0.5; - break; - } - } - - return pitchDegree; + return scale.pitchToDegreeMap[(pitch + 12 - tonicPitch) % 12]; }; // Returns the row index where a note of the given pitch would be placed, // according to the given key. May return fractional values, which // indicates that the pitch falls between scale degrees. - this.getRowForPitch = function(pitch, key) + this.getRowForPitch = function(pitch, scale, tonicPitch) { - var pitchOctave = theory.getOctaveForPitch(pitch); - var pitchInOctave = ((pitch + 12 - key.tonicPitch) % 12); - var pitchDegree = key.scale.degrees.length - 0.5; + var pitchOctave = theory.getOctaveForPitch(pitch - tonicPitch); + var pitchDegree = theory.getDegreeForPitch(pitch, scale, tonicPitch); - var degreeOffsetFromC = 0; - if (key.tonicPitch != 0) - { - var originalTonicPitch = key.tonicPitch; - key.tonicPitch = 0; - degreeOffsetFromC = Math.floor(this.getRowForPitch(originalTonicPitch, key)); - key.tonicPitch = originalTonicPitch; - } + var offset = 0; + if (tonicPitch != 0) + offset = Math.floor(this.getRowForPitch(tonicPitch, scale, 0)); - for (var i = 0; i < key.scale.degrees.length; i++) - { - if (key.scale.degrees[i] == pitchInOctave) - { - pitchDegree = i; - break; - } - else if (key.scale.degrees[i] > pitchInOctave) - { - pitchDegree = i - 0.5; - break; - } - } - return pitchDegree + degreeOffsetFromC + (Math.floor((pitch - key.tonicPitch) / 12) * key.scale.degrees.length); + return pitchDegree + offset + pitchOctave * 7; }; // Returns the pitch of the given row index, according to the given key. - this.getPitchForRow = function(row, key) + this.getPitchForRow = function(row, scale, tonicPitch) { - var degreeOffsetFromC = 0; - if (key.tonicPitch != 0) - { - var originalTonicPitch = key.tonicPitch; - key.tonicPitch = 0; - degreeOffsetFromC = Math.floor(this.getRowForPitch(originalTonicPitch, key)); - key.tonicPitch = originalTonicPitch; - } + var rowOctave = Math.floor(row / 7); + + var offset = 0; + if (tonicPitch != 0) + offset = Math.floor(theory.getRowForPitch(tonicPitch, scale, 0)); - return key.scale.degrees[(row + key.scale.degrees.length * 3 - degreeOffsetFromC) % key.scale.degrees.length] + key.tonicPitch + Math.floor((row - degreeOffsetFromC) / key.scale.degrees.length) * 12; + return scale.pitches[Math.floor(row + 7 - offset) % 7] + tonicPitch + Math.floor((row - offset) / 7) * 12; }; diff --git a/src/toolbox.js b/src/toolbox.js index a1860ef..2e33ac0 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -444,7 +444,7 @@ Toolbox.prototype.refresh = function() for (var i = 0; i < 12; i++) { var pitch = i; - this.keyChangeTonicOptions[i].innerHTML = theory.getNameForPitch(pitch, keyChange); + this.keyChangeTonicOptions[i].innerHTML = theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); } this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; @@ -473,9 +473,9 @@ Toolbox.prototype.refresh = function() for (var i = 0; i < 12; i++) { var pitch = (i + keyChange.tonicPitch) % 12; - this.buttonNotes[i].innerHTML = theory.getNameForPitch(pitch, keyChange.scale); + this.buttonNotes[i].innerHTML = theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); - var degree = theory.getDegreeForPitch(pitch, keyChange); + var degree = theory.getDegreeForPitch(pitch, keyChange.scale, keyChange.tonicPitch); if ((degree % 1) == 0) { @@ -522,19 +522,19 @@ Toolbox.prototype.refresh = function() { for (var i = 0; i < 7; i++) { - var pitch1 = keyChange.tonicPitch + keyChange.scale.degrees[i]; + var pitch1 = keyChange.tonicPitch + keyChange.scale.pitches[i]; - var pitch2 = keyChange.tonicPitch + keyChange.scale.degrees[(i + 2) % keyChange.scale.degrees.length]; - if ((i + 2) >= keyChange.scale.degrees.length) + var pitch2 = keyChange.tonicPitch + keyChange.scale.pitches[(i + 2) % 7]; + if ((i + 2) >= 7) pitch2 += 12; - var pitch3 = keyChange.tonicPitch + keyChange.scale.degrees[(i + 4) % keyChange.scale.degrees.length]; - if ((i + 4) >= keyChange.scale.degrees.length) + var pitch3 = keyChange.tonicPitch + keyChange.scale.pitches[(i + 4) % 7]; + if ((i + 4) >= 7) pitch3 += 12; var color = theory.getColorForDegree(i); var chord = theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); - var numeral = theory.getRomanNumeralForPitch(pitch1, keyChange); + var numeral = theory.getRomanNumeralForPitch(pitch1, keyChange.scale, keyChange.tonicPitch); var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); romanText += "" + chord.romanSup + ""; @@ -582,7 +582,7 @@ Toolbox.prototype.refresh = function() for (var i = 0; i < 12; i++) { var rootPitch = (keyChange.tonicPitch + i) % 12; - var degree = theory.getDegreeForPitch(rootPitch, keyChange); + var degree = theory.getDegreeForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); var index = inKeyIndex; if ((degree % 1) != 0) @@ -590,7 +590,7 @@ Toolbox.prototype.refresh = function() var color = theory.getColorForDegree(degree); var chord = theory.chords[chordCategory - 1]; - var numeral = theory.getRomanNumeralForPitch(rootPitch, keyChange); + var numeral = theory.getRomanNumeralForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); romanText += "" + chord.romanSup + ""; From a7090d0e4515add5f91cdeb0d69cfc74e8ffd857 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 23 Apr 2016 23:57:08 -0300 Subject: [PATCH 22/46] WIP: improve editor --- index.html | 1 + main.js | 38 ++- src/editor.js | 23 +- src/editor_drawing.js | 376 ++++++++------------- src/editor_event_handling.js | 555 +++++++++++++++++++++++++++++++ src/editor_interaction.js | 609 +++-------------------------------- src/editor_representation.js | 299 ++++++----------- src/songdata.js | 59 +++- src/theory.js | 11 +- 9 files changed, 927 insertions(+), 1044 deletions(-) create mode 100644 src/editor_event_handling.js diff --git a/index.html b/index.html index ac3e48a..1ac5f1c 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,7 @@ + diff --git a/main.js b/main.js index 6621338..e283df7 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,33 @@ +var mainEditor; + function testOnLoad() { - var song = new SongData(); - song.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); + var theory = new Theory(); + + var song = new SongData(theory); + + song.addKeyChange(new SongDataKeyChange(960 * 2, theory.scales[0], theory.D)); + song.addMeterChange(new SongDataMeterChange(960 * 2, 3, 4)); + song.addSectionBreak(new SongSectionBreak(960 * 1.5)); + + song.addNote(new SongDataNote(0, 960 / 4, theory.C + 60)); + song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.D + 60)); + song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); + song.addNote(new SongDataNote(960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); + song.addNote(new SongDataNote(960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.G + 60)); + song.addNote(new SongDataNote(960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.A + 60)); + song.addNote(new SongDataNote(960, 960, theory.B + 60)); + + song.addNote(new SongDataNote(960 * 2, 960 / 4, theory.C + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4, 960 / 16, theory.D + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.G + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.A + 60)); + song.addNote(new SongDataNote(960 * 2 + 960, 960 / 2, theory.B + 60)); + song.addNote(new SongDataNote(960 * 2 + 960 + 960 / 2, 960 / 2, theory.C + 72)); + + /*song.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); song.addMeterChange(new SongDataMeterChange(0, 4, 4)); song.addKeyChange(new SongDataKeyChange(960 * 2, theory.scales[8], theory.C)); song.addKeyChange(new SongDataKeyChange(960 * 4, theory.scales[0], theory.C)); @@ -34,14 +60,14 @@ function testOnLoad() song.addNote(new SongDataNote(960 * 2.5, 960 / 16, theory.Gs + 60)); song.addNote(new SongDataNote(960 * 2.5 + 960 / 16, 960 / 16, theory.C + 60 + 12)); song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 2, 960 / 16, theory.Cs + 60 + 12)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 60 + 12)); + song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 60 + 12));*/ var synth = new Synth(); setInterval(function() { synth.process(6); }, 1000 / 60); var canvas = document.getElementById("editorCanvas"); - var editor = new SongEditor(canvas, song, synth); + var editor = new SongEditor(canvas, theory, synth, song); editor.refreshCanvas(); - - var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor, synth); + mainEditor = editor; + //var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor, synth); } \ No newline at end of file diff --git a/src/editor.js b/src/editor.js index 61c0279..75351c1 100644 --- a/src/editor.js +++ b/src/editor.js @@ -1,4 +1,4 @@ -function SongEditor(canvas, songData, synth) +function SongEditor(canvas, theory, synth, songData) { this.canvas = canvas; this.ctx = canvas.getContext("2d"); @@ -30,13 +30,9 @@ function SongEditor(canvas, songData, synth) // The tick grid which the cursor is snapped to. this.tickSnap = 960 / 16; - // These arrays store representation objects, which tell things like + // This stores sections' representation objects, which tell things like // the object positioning/layout, and where they can be interacted with the mouse. - this.viewBlocks = []; - this.viewNotes = []; - this.viewChords = []; - this.viewKeyChanges = []; - this.viewMeterChanges = []; + this.viewRegions = []; // These control scrolling. this.rowAtCenter = 8 * 5; @@ -69,25 +65,24 @@ function SongEditor(canvas, songData, synth) this.hoverStretchL = false; // Layout constants. - this.WHOLE_NOTE_DURATION = 960; this.MARGIN_LEFT = 16; this.MARGIN_RIGHT = 16; this.MARGIN_TOP = 4; this.MARGIN_BOTTOM = 8; - this.HEADER_MARGIN = 40; + this.SECTION_SEPARATION = 10; + this.HEADER_HEIGHT = 40; this.NOTE_HEIGHT = 14; this.NOTE_MARGIN_HOR = 0.5; this.NOTE_MARGIN_VER = 0.5; this.NOTE_STRETCH_MARGIN = 2; this.CHORD_HEIGHT = 60; - this.CHORDNOTE_MARGIN = 10; + this.CHORD_NOTE_SEPARATION = 10; this.CHORD_ORNAMENT_HEIGHT = 5; - this.KEYCHANGE_BAR_WIDTH = 27; - this.METERCHANGE_BAR_WIDTH = 4; - // Finally, set the song data, and the synth manager. - this.setData(songData); + // Finally, set the song data, and external dependencies. + this.theory = theory; this.synth = synth; + this.setData(songData); } diff --git a/src/editor_drawing.js b/src/editor_drawing.js index e30c632..2d799de 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -10,43 +10,136 @@ SongEditor.prototype.refreshCanvas = function() // Draw blocks. var CURSOR_COLOR = "#0000ff"; - var BLOCK_BORDER_COLOR = "#000000"; - var BLOCK_MEASURE_COLOR = "#dddddd"; - var BLOCK_MEASURE_COLOR_STRONG = "#888888"; + var SECTION_BORDER_COLOR = "#000000"; + var NOTE_LINE_COLOR = "#dddddd"; + var MEASURE_COLOR = "#dddddd"; + var MEASURE_COLOR_STRONG = "#888888"; + var KEY_CHANGE_COLOR = "#aaaaaa"; + var METER_CHANGE_COLOR = "#88aaaa"; - for (var i = 0; i < this.viewBlocks.length; i++) + this.ctx.save(); + this.ctx.beginPath(); + + for (var r = 0; r < this.viewRegions.length; r++) { - var block = this.viewBlocks[i]; + var region = this.viewRegions[r]; - // Don't draw bars out of the screen (with margin for between-block note parts). - if (block.x1 > this.canvasWidth || - block.x2 < -this.KEYCHANGE_BAR_WIDTH) - continue; + // Draw region boundaries. + this.ctx.strokeStyle = SECTION_BORDER_COLOR; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); - var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); - var firstVisibleRowY = this.getYForRow(block, visibleRows.first); - var lastVisibleRowY = this.getYForRow(block, visibleRows.last + 1); + this.ctx.strokeRect( + region.x1, + region.y1 + this.HEADER_HEIGHT, + region.x2 - region.x1, + (region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - (region.y1 + this.HEADER_HEIGHT)); + + this.ctx.strokeRect( + region.x1, + region.y2 - this.CHORD_HEIGHT, + region.x2 - region.x1, + this.CHORD_HEIGHT); + + this.ctx.stroke(); - this.ctx.save(); + // Draw note lines. + this.ctx.strokeStyle = NOTE_LINE_COLOR; this.ctx.beginPath(); + for (var n = 1; n < region.highestNoteRow - region.lowestNoteRow; n++) + { + var y = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - n * this.NOTE_HEIGHT; + this.ctx.moveTo(region.x1, y); + this.ctx.lineTo(region.x2, y); + } + this.ctx.stroke(); + + // Draw beat lines. + var beatCount = 0; + for (var n = region.meter.tick - region.tick; n < region.duration; n += this.songData.ticksPerWholeNote / region.meter.denominator) + { + if (n != 0) + { + this.ctx.strokeStyle = (beatCount == 0 ? MEASURE_COLOR_STRONG : MEASURE_COLOR); + this.ctx.beginPath(); + this.ctx.moveTo( + region.x1 + n * this.tickZoom, + region.y1 + this.HEADER_HEIGHT); + this.ctx.lineTo( + region.x1 + n * this.tickZoom, + region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION); + this.ctx.moveTo( + region.x1 + n * this.tickZoom, + region.y2 - this.CHORD_HEIGHT); + this.ctx.lineTo( + region.x1 + n * this.tickZoom, + region.y2); + this.ctx.stroke(); + } + + beatCount = (beatCount + 1) % region.meter.numerator; + } - var clipY1 = Math.max(block.y1, lastVisibleRowY); - var clipY2 = Math.min(block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN, firstVisibleRowY); - this.ctx.rect(0, clipY1, this.canvasWidth, clipY2 - clipY1); - this.ctx.clip(); + // Draw notes. + for (var n = 0; n < region.notes.length; n++) + { + var noteIndex = region.notes[n]; + var note = this.songData.notes[noteIndex]; + + if (!this.noteSelections[noteIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") + this.drawNote(region, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); + } - // Draw rows. - for (var row = visibleRows.first; row <= visibleRows.last + 1; row++) + // Draw dragged notes. + if (this.mouseDragAction != null && this.mouseDragAction != "scroll") { - this.ctx.strokeStyle = (theory.getPitchForRow(row, block.key.scale, block.key.tonicPitch) % 12 == block.key.tonicPitch ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); - this.ctx.lineWidth = 2; + for (var n = 0; n < this.songData.notes.length; n++) + { + if (!this.noteSelections[n]) + continue; + + var note = this.songData.notes[n]; + var noteDragged = this.getNoteDragged(note, this.mouseDragCurrent); + this.drawNote(region, noteDragged.pitch, noteDragged.tick, noteDragged.duration, noteIndex == this.hoverNote, true); + } + } + + // Draw key change. + if (region.showKeyChange) + { + this.ctx.strokeStyle = KEY_CHANGE_COLOR; this.ctx.beginPath(); - this.ctx.moveTo(block.x1, this.getYForRow(block, row)); - this.ctx.lineTo(block.x2, this.getYForRow(block, row)); + this.ctx.moveTo(region.x1, region.y1); + this.ctx.lineTo(region.x1, region.y2); this.ctx.stroke(); + + this.ctx.font = "14px Tahoma"; + this.ctx.fillStyle = KEY_CHANGE_COLOR; + this.ctx.fillText( + this.theory.getNameForPitch(region.key.tonicPitch, region.key.scale, region.key.tonicPitch) + " " + region.key.scale.name, + region.x1 + 8, + region.y1 + 12); } - // Draw note measures. + // Draw meter change. + if (region.showMeterChange) + { + this.ctx.strokeStyle = METER_CHANGE_COLOR; + this.ctx.beginPath(); + this.ctx.moveTo(region.x1, region.y1 + 20); + this.ctx.lineTo(region.x1, region.y2); + this.ctx.stroke(); + + this.ctx.font = "14px Tahoma"; + this.ctx.fillStyle = METER_CHANGE_COLOR; + this.ctx.fillText( + "" + region.meter.numerator + " / " + region.meter.denominator, + region.x1 + 8, + region.y1 + 32); + } + } + + /*// Draw note measures. var submeasureCount = 0; for (var n = block.meter.tick - block.tick; n < block.duration; n += this.WHOLE_NOTE_DURATION / block.meter.denominator) { @@ -128,13 +221,6 @@ SongEditor.prototype.refreshCanvas = function() } } - // Draw borders. - this.ctx.strokeStyle = BLOCK_BORDER_COLOR; - this.ctx.lineWidth = 2; - - this.ctx.strokeRect(block.x1, block.y1, block.x2 - block.x1, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - block.y1); - this.ctx.strokeRect(block.x1, block.y2 - this.CHORD_HEIGHT, block.x2 - block.x1, this.CHORD_HEIGHT); - // Draw cursor. if (this.showCursor && this.cursorTick >= block.tick && this.cursorTick < block.tick + block.duration) { @@ -170,171 +256,7 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.lineTo(cursorX + 6, cursorY2 + 6); this.ctx.lineTo(cursorX, cursorY2); this.ctx.fill(); - } - } - - - // Draw key changes. - this.ctx.font = "14px Tahoma"; - this.ctx.textAlign = "left"; - this.ctx.textBaseline = "top"; - - var KEY_BORDER_COLOR = "#aaaaaa"; - var KEY_BORDER_COLOR_H = "#cccccc"; - var KEY_FILL_COLOR_SEL = "#eeeeee"; - var KEY_PITCH_COLOR = "#444444"; - - for (var i = 0; i < this.viewKeyChanges.length; i++) - { - var keyChange = this.viewKeyChanges[i]; - - if (this.keyChangeSelections[i]) - { - if (this.mouseDragAction != null) - { - var draggedKeyChange = this.getKeyChangeDragged(keyChange, this.mouseDragCurrent); - - // Draw dragging-but-not-moved. - if (draggedKeyChange.tick == keyChange.tick) - { - this.ctx.fillStyle = KEY_FILL_COLOR_SEL; - this.ctx.fillRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - this.ctx.strokeStyle = KEY_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - } - // Draw dragging. - else - { - var x = this.getPositionForTick(draggedKeyChange.tick); - this.ctx.strokeStyle = KEY_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - this.ctx.moveTo(x, keyChange.y1); - this.ctx.lineTo(x, keyChange.y2); - this.ctx.stroke(); - } - } - // Draw selected. - else - { - this.ctx.fillStyle = KEY_FILL_COLOR_SEL; - this.ctx.fillRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - this.ctx.strokeStyle = KEY_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - } - } - // Draw idle/hover. - else - { - this.ctx.strokeStyle = (this.hoverKeyChange == i ? KEY_BORDER_COLOR_H : KEY_BORDER_COLOR); - this.ctx.lineWidth = 2; - this.ctx.strokeRect(keyChange.x1, keyChange.y1, keyChange.x2 - keyChange.x1, keyChange.y2 - keyChange.y1); - } - - // Draw key name. - var textX = keyChange.x2 + 8; - this.ctx.font = "14px Tahoma"; - var songKeyChange = this.songData.keyChanges[keyChange.keyChangeIndex]; - this.ctx.fillStyle = KEY_BORDER_COLOR; - this.ctx.fillText( - "" + theory.getNameForPitch(songKeyChange.tonicPitch, songKeyChange.scale, songKeyChange.tonicPitch) + " " + songKeyChange.scale.name, - textX, - keyChange.y1, - this.viewBlocks[keyChange.blockIndex].x2 - keyChange.x1 - 32); - - // Draw pitches. - this.ctx.font = "10px Tahoma"; - - var visibleRows = this.getFirstAndLastVisibleRowsForBlock(block); - for (var row = visibleRows.first; row <= visibleRows.last; row++) - { - var pitch = theory.getPitchForRow(row, songKeyChange.scale, songKeyChange.tonicPitch); - var y = this.getYForRow(this.viewBlocks[keyChange.blockIndex], row + 1); - - this.ctx.fillStyle = KEY_PITCH_COLOR; - this.ctx.fillText( - "" + theory.getNameForPitch(pitch, songKeyChange.scale, songKeyChange.tonicPitch), - keyChange.x1 + 4, - y); - - this.ctx.fillText( - "" + theory.getOctaveForPitch(pitch), - keyChange.x1 + 18, - y); - } - } - - - // Draw meter changes. - this.ctx.font = "14px Tahoma"; - this.ctx.textAlign = "left"; - - var METER_BORDER_COLOR = "#88aaaa"; - var METER_BORDER_COLOR_HOVER = "#bbdddd"; - var METER_FILL_COLOR_SEL = "#bbdddd"; - - for (var i = 0; i < this.viewMeterChanges.length; i++) - { - var meterChange = this.viewMeterChanges[i]; - - if (this.meterChangeSelections[i]) - { - if (this.mouseDragAction != null) - { - var draggedMeterChange = this.getMeterChangeDragged(meterChange, this.mouseDragCurrent); - - // Draw dragging-but-not-moved. - if (draggedMeterChange.tick == meterChange.tick) - { - this.ctx.fillStyle = METER_FILL_COLOR_SEL; - this.ctx.fillRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - this.ctx.strokeStyle = METER_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - } - // Draw dragging. - else - { - var x = this.getPositionForTick(draggedMeterChange.tick); - this.ctx.strokeStyle = METER_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - this.ctx.moveTo(x, meterChange.y1); - this.ctx.lineTo(x, meterChange.y2); - this.ctx.stroke(); - } - } - // Draw selected. - else - { - this.ctx.fillStyle = METER_FILL_COLOR_SEL; - this.ctx.fillRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - this.ctx.strokeStyle = METER_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - } - } - // Draw idle/hover. - else - { - this.ctx.strokeStyle = (this.hoverMeterChange == i ? METER_BORDER_COLOR_HOVER : METER_BORDER_COLOR); - this.ctx.lineWidth = 2; - this.ctx.strokeRect(meterChange.x1, meterChange.y1, meterChange.x2 - meterChange.x1, meterChange.y2 - meterChange.y1); - } - - // Draw numbers. - var textX = meterChange.x2 + 8; - var songMeterChange = this.songData.meterChanges[meterChange.meterChangeIndex]; - this.ctx.fillStyle = METER_BORDER_COLOR; - this.ctx.fillText( - "" + songMeterChange.numerator + " / " + songMeterChange.denominator, - textX, - meterChange.y1, - this.viewBlocks[meterChange.blockIndex].x2 - meterChange.x1 - 16); - } - + }*/ // Draw side fade-outs. var leftFadeGradient = this.ctx.createLinearGradient(0, 0, this.MARGIN_LEFT, 0); @@ -354,22 +276,23 @@ SongEditor.prototype.refreshCanvas = function() } -SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hovering, selected) +SongEditor.prototype.drawNote = function(region, pitch, tick, duration, hovering, selected) { - var block = this.viewBlocks[blockIndex]; - - // Check if the note is inside the block. - if (tick + duration <= block.tick || - tick >= block.tick + block.duration) + // Check if the note is inside the region. + if (tick + duration <= region.tick || + tick >= region.tick + region.duration) return; - - var scrollY = -this.rowAtCenter * this.NOTE_HEIGHT; - var deg = theory.getDegreeForPitch(pitch, block.key.scale, block.key.tonicPitch); - var row = theory.getRowForPitch(pitch, block.key.scale, block.key.tonicPitch); - var pos = this.getNotePosition(block, row, tick, duration); + var deg = this.theory.getDegreeForPitch(pitch, region.key.scale, region.key.tonicPitch); + var row = this.theory.getRowForPitch(pitch, region.key.scale, region.key.tonicPitch); - this.drawDegreeColoredRectangle(deg, pos); + var clippedStartTick = Math.max(tick, region.tick); + var clippedEndTick = Math.min(tick + duration, region.tick + region.duration); + var clippedDuration = clippedEndTick - clippedStartTick; + + var pos = this.getNotePosition(region, row, clippedStartTick, clippedDuration); + + this.drawDegreeColoredRectangle(deg, pos.x1, pos.y1, pos.x2, pos.y2); // Draw highlights. this.ctx.save(); @@ -389,27 +312,6 @@ SongEditor.prototype.drawNote = function(blockIndex, pitch, tick, duration, hove } this.ctx.restore(); - - // Draw possibly-bent part between blocks. - if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) - { - var nextBlock = this.viewBlocks[blockIndex + 1]; - var nextRow = theory.getRowForPitch(pitch, nextBlock.key.scale, nextBlock.key.tonicPitch); - var nextPos = this.getNotePosition(block, nextRow, tick, duration); - - var col = theory.getColorForDegree(deg - (deg % 1)); - - this.ctx.save(); - this.ctx.globalAlpha = 0.5; - this.ctx.fillStyle = col; - this.ctx.beginPath(); - this.ctx.moveTo(block.x2, pos.y1); - this.ctx.lineTo(nextBlock.x1, nextPos.y1); - this.ctx.lineTo(nextBlock.x1, nextPos.y2); - this.ctx.lineTo(block.x2, pos.y2); - this.ctx.fill(); - this.ctx.restore(); - } } @@ -495,28 +397,28 @@ SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hov } -SongEditor.prototype.drawDegreeColoredRectangle = function(deg, pos) +SongEditor.prototype.drawDegreeColoredRectangle = function(deg, x1, y1, x2, y2) { - var col = theory.getColorForDegree(deg - (deg % 1)); + var col = this.theory.getColorForDegree(deg - (deg % 1)); this.ctx.save(); this.ctx.beginPath(); - this.ctx.rect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.rect(x1, y1, x2 - x1, y2 - y1); this.ctx.clip(); this.ctx.fillStyle = col; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); + this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); // Draw stripes for fractional scale degrees. if ((deg % 1) != 0) { - this.ctx.strokeStyle = theory.getColorForDegree((deg - (deg % 1) + 1) % 7); + this.ctx.strokeStyle = this.theory.getColorForDegree((deg - (deg % 1) + 1) % 7); this.ctx.lineWidth = 5; - for (var i = 0; i < pos.x2 - pos.x1 + 30; i += 15) + for (var i = 0; i < x2 - x1 + 30; i += 15) { this.ctx.beginPath(); - this.ctx.moveTo(pos.x1 + i, pos.y1 - 5); - this.ctx.lineTo(pos.x1 + i - 20, pos.y1 - 5 + 20); + this.ctx.moveTo(x1 + i, y1 - 5); + this.ctx.lineTo(x1 + i - 20, y1 - 5 + 20); this.ctx.stroke(); } } diff --git a/src/editor_event_handling.js b/src/editor_event_handling.js new file mode 100644 index 0000000..aaaaa86 --- /dev/null +++ b/src/editor_event_handling.js @@ -0,0 +1,555 @@ +function transformMousePosition(elem, ev) +{ + var rect = elem.getBoundingClientRect(); + return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; +} + + +SongEditor.prototype.handleMouseMove = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + if (!this.interactionEnabled) + return; + + this.canvas.style.cursor = "default"; + this.mouseDragCurrent = mousePos; + this.clearHover(); + + // Check what's under the mouse, if it's not down. + if (!this.mouseDown) + { + for (var r = 0; r < this.viewRegions.length; r++) + { + var region = this.viewRegions[r]; + + if (isPointInside(mousePos, region)) + { + // Check for notes. + for (var n = 0; n < region.notes.length; n++) + { + var note = this.songData.notes[region.notes[n]]; + var noteRow = this.theory.getRowForPitch(note.pitch, region.key.scale, region.key.tonicPitch); + var notePos = this.getNotePosition(region, noteRow, note.tick, note.duration); + + if (isPointInside(mousePos, notePos)) + { + this.hoverNote = region.notes[n]; + + if (mousePos.x <= notePos.x1 + this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchL = true; + } + else if (mousePos.x >= notePos.x2 - this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchR = true; + } + else + this.canvas.style.cursor = "pointer"; + + break; + } + } + + // Check for chords. + /*if (this.hoverNote < 0) + { + for (var n = 0; n < this.viewBlocks[b].chords.length; n++) + { + var chord = this.viewBlocks[b].chords[n]; + if (isPointInside(mousePos, chord)) + { + this.hoverChord = chord.chordIndex; + + if (mousePos.x <= chord.resizeHandleL + this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchL = true; + } + else if (mousePos.x >= chord.resizeHandleR - this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchR = true; + } + else + this.canvas.style.cursor = "pointer"; + + break; + } + } + }*/ + + break; + } + } + + // Check for key changes. + /*if (this.hoverNote < 0 && this.hoverChord < 0) + { + for (var n = 0; n < this.viewKeyChanges.length; n++) + { + var keyChange = this.viewKeyChanges[n]; + if (isPointInside(mousePos, keyChange)) + { + this.hoverKeyChange = keyChange.keyChangeIndex; + this.canvas.style.cursor = "ew-resize"; + break; + } + } + } + + // Check for meter changes. + if (this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) + { + for (var n = 0; n < this.viewMeterChanges.length; n++) + { + var meterChange = this.viewMeterChanges[n]; + if (isPointInside(mousePos, meterChange)) + { + this.hoverMeterChange = meterChange.meterChangeIndex; + this.canvas.style.cursor = "ew-resize"; + break; + } + } + }*/ + + this.refreshCanvas(); + } + + else if (this.mouseDragAction == "move") + { + this.canvas.style.cursor = "move"; + this.refreshCanvas(); + } + + else if (this.mouseDragAction == "stretch") + { + this.canvas.style.cursor = "ew-resize"; + this.refreshCanvas(); + } + + else if (this.mouseDragAction == "scroll") + { + /*var rowOffset = (mousePos.y - this.mouseDragOrigin.y) / this.NOTE_HEIGHT; + this.rowAtCenter += rowOffset; + var rowLimits = this.getFirstAndLastScrollableRows(); + this.rowAtCenter = Math.min(rowLimits.first, Math.max(rowLimits.last, this.rowAtCenter)); + this.mouseDragOrigin.y = mousePos.y; + + var xOffset = (this.mouseDragOrigin.x - mousePos.x); + this.xAtLeft += xOffset; + this.xAtLeft = Math.max(0, Math.min(this.viewBlocks[this.viewBlocks.length - 1].x2 + this.canvasWidth / 2, this.xAtLeft)); + this.mouseDragOrigin.x = mousePos.x; + + this.refreshRepresentation();*/ + this.refreshCanvas(); + } +} + + +SongEditor.prototype.handleMouseDown = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + if (!this.interactionEnabled) + return; + + if (!ev.ctrlKey) + this.unselectAll(); + + this.mouseDragAction = null; + this.mouseDragOrigin = mousePos; + + this.cursorTick = this.getTickAtPosition(mousePos); + this.cursorZone = this.getZoneAtPosition(mousePos); + this.showCursor = true; + + // Start a dragging operation. + if (this.hoverNote >= 0) + { + this.showCursor = false; + if (!this.noteSelections[this.hoverNote]) + this.selectedObjects++; + + this.noteSelections[this.hoverNote] = true; + this.cursorTick = this.songData.notes[this.hoverNote].tick; + this.theory.playNoteSample(this.synth, this.songData.notes[this.hoverNote].pitch); + + if (this.hoverStretchR) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick + this.songData.notes[this.hoverNote].duration; + this.mouseStretchPivotTick = this.getEarliestSelectedTick(); + } + else if (this.hoverStretchL) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick; + this.mouseStretchPivotTick = this.getLatestSelectedTick(); + } + else + this.mouseDragAction = "move"; + } + else if (this.hoverChord >= 0) + { + this.showCursor = false; + if (!this.chordSelections[this.hoverChord]) + this.selectedObjects++; + + this.chordSelections[this.hoverChord] = true; + this.cursorTick = this.songData.chords[this.hoverChord].tick; + theory.playChordSample(this.synth, this.songData.chords[this.hoverChord].chord, this.songData.chords[this.hoverChord].rootPitch); + + if (this.hoverStretchR) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick + this.songData.chords[this.hoverChord].duration; + this.mouseStretchPivotTick = this.getEarliestSelectedTick(); + } + else if (this.hoverStretchL) + { + this.mouseDragAction = "stretch"; + this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick; + this.mouseStretchPivotTick = this.getLatestSelectedTick(); + } + else + this.mouseDragAction = "move"; + } + else if (this.hoverKeyChange >= 0) + { + if (!this.keyChangeSelections[this.hoverKeyChange]) + this.selectedObjects++; + + this.showCursor = false; + this.keyChangeSelections[this.hoverKeyChange] = true; + this.mouseDragAction = "move"; + } + else if (this.hoverMeterChange >= 0) + { + if (!this.meterChangeSelections[this.hoverMeterChange]) + this.selectedObjects++; + + this.showCursor = false; + this.meterChangeSelections[this.hoverMeterChange] = true; + this.mouseDragAction = "move"; + } + else + { + this.mouseDragAction = "scroll"; + this.clearHover(); + } + + this.mouseDown = true; + this.callOnSelectionChanged(); + this.refreshCanvas(); + this.callOnCursorChanged(); +} + + +SongEditor.prototype.handleMouseUp = function(ev) +{ + ev.preventDefault(); + var mousePos = transformMousePosition(this.canvas, ev); + + if (!this.interactionEnabled) + return; + + // Apply dragged modifications. + if (this.mouseDown && this.mouseDragAction != null && this.mouseDragAction != "scroll") + { + // Store modified objects in a local array and + // remove them from the song data. + var selectedNotes = []; + for (var n = this.noteSelections.length - 1; n >= 0; n--) + { + if (this.noteSelections[n]) + { + selectedNotes.push(this.songData.notes[n]); + this.songData.notes.splice(n, 1); + } + } + + var selectedChords = []; + for (var n = this.chordSelections.length - 1; n >= 0; n--) + { + if (this.chordSelections[n]) + { + selectedChords.push(this.songData.chords[n]); + this.songData.chords.splice(n, 1); + } + } + + var selectedKeyChanges = []; + for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) + { + if (this.keyChangeSelections[n]) + { + selectedKeyChanges.push(this.songData.keyChanges[n]); + this.songData.keyChanges.splice(n, 1); + } + } + + var selectedMeterChanges = []; + for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) + { + if (this.meterChangeSelections[n]) + { + selectedMeterChanges.push(this.songData.meterChanges[n]); + this.songData.meterChanges.splice(n, 1); + } + } + + // Apply the modification and reinsert objects into the song data. + var newNotes = []; + for (var n = 0; n < selectedNotes.length; n++) + { + var draggedNote = this.getNoteDragged(selectedNotes[n], mousePos); + var newNote = new SongDataNote(draggedNote.tick, draggedNote.duration, draggedNote.pitch); + newNotes.push(newNote); + this.songData.addNote(newNote); + } + + var newChords = []; + for (var n = 0; n < selectedChords.length; n++) + { + var draggedChord = this.getChordDragged(selectedChords[n], mousePos); + var newChord = new SongDataChord(draggedChord.tick, draggedChord.duration, selectedChords[n].chord, selectedChords[n].rootPitch); + newChords.push(newChord); + this.songData.addChord(newChord); + } + + var newKeyChanges = []; + for (var n = 0; n < selectedKeyChanges.length; n++) + { + var draggedKeyChange = this.getKeyChangeDragged(selectedKeyChanges[n], mousePos); + var newKeyChange = new SongDataKeyChange(draggedKeyChange.tick, selectedKeyChanges[n].scale, selectedKeyChanges[n].tonicPitch); + newKeyChanges.push(newKeyChange); + this.songData.addKeyChange(newKeyChange); + } + + var newMeterChanges = []; + for (var n = 0; n < selectedMeterChanges.length; n++) + { + var draggedMeterChange = this.getMeterChangeDragged(selectedMeterChanges[n], mousePos); + var newMeterChange = new SongDataMeterChange(draggedMeterChange.tick, selectedMeterChanges[n].numerator, selectedMeterChanges[n].denominator); + newMeterChanges.push(newMeterChange); + this.songData.addMeterChange(newMeterChange); + } + + this.clearHover(); + this.refreshRepresentation(); + + // Find which objects were selected before, and select them again. + for (var n = 0; n < this.songData.notes.length; n++) + { + for (var m = 0; m < newNotes.length; m++) + { + if (this.songData.notes[n] == newNotes[m]) + { + this.selectedObjects++; + this.noteSelections[n] = true; + } + } + } + + for (var n = 0; n < this.songData.chords.length; n++) + { + for (var m = 0; m < newChords.length; m++) + { + if (this.songData.chords[n] == newChords[m]) + { + this.selectedObjects++; + this.chordSelections[n] = true; + } + } + } + + for (var n = 0; n < this.songData.keyChanges.length; n++) + { + for (var m = 0; m < newKeyChanges.length; m++) + { + if (this.songData.keyChanges[n] == newKeyChanges[m]) + { + this.selectedObjects++; + this.keyChangeSelections[n] = true; + } + } + } + + for (var n = 0; n < this.songData.meterChanges.length; n++) + { + for (var m = 0; m < newMeterChanges.length; m++) + { + if (this.songData.meterChanges[n] == newMeterChanges[m]) + { + this.selectedObjects++; + this.meterChangeSelections[n] = true; + } + } + } + } + + this.mouseDown = false; + this.mouseDragAction = null; + this.refreshCanvas(); +} + + +SongEditor.prototype.handleKeyDown = function(ev) +{ + if (!this.interactionEnabled) + return; + + var keyCode = (ev.key || ev.which || ev.keyCode); + + if (keyCode == 46 || (keyCode == 8 && this.selectedObjects > 0)) // Delete/Backspace + { + ev.preventDefault(); + + // Remove selected objects from the song data. + var selectedNotes = []; + for (var n = this.noteSelections.length - 1; n >= 0; n--) + { + if (this.noteSelections[n]) + { + selectedNotes.push(this.songData.notes[n]); + this.songData.notes.splice(n, 1); + } + } + + var selectedChords = []; + for (var n = this.chordSelections.length - 1; n >= 0; n--) + { + if (this.chordSelections[n]) + { + selectedChords.push(this.songData.chords[n]); + this.songData.chords.splice(n, 1); + } + } + + var selectedKeyChanges = []; + for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) + { + if (this.keyChangeSelections[n]) + { + selectedKeyChanges.push(this.songData.keyChanges[n]); + this.songData.keyChanges.splice(n, 1); + } + } + + var selectedMeterChanges = []; + for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) + { + if (this.meterChangeSelections[n]) + { + selectedMeterChanges.push(this.songData.meterChanges[n]); + this.songData.meterChanges.splice(n, 1); + } + } + + this.showCursor = true; + this.clearHover(); + this.refreshRepresentation(); + this.refreshCanvas(); + this.callOnSelectionChanged(); + } + + else if (keyCode == 8 && this.selectedObjects == 0) // Backspace + { + ev.preventDefault(); + + var lastTickToConsider = 0; + var considerNotes = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_NOTES); + var considerChords = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_CHORDS); + + if (considerNotes) + { + for (var i = 0; i < this.songData.notes.length; i++) + { + var note = this.songData.notes[i]; + if (note.tick + note.duration > lastTickToConsider) + lastTickToConsider = Math.min(note.tick + note.duration, this.cursorTick); + } + } + + if (considerChords) + { + for (var i = 0; i < this.songData.chords.length; i++) + { + var chord = this.songData.chords[i]; + if (chord.tick + chord.duration > lastTickToConsider) + lastTickToConsider = Math.min(chord.tick + chord.duration, this.cursorTick); + } + } + + if (lastTickToConsider != this.cursorTick) + { + this.cursorTick = Math.max(0, lastTickToConsider); + + this.clearHover(); + this.refreshCanvas(); + this.callOnCursorChanged(); + return; + } + + var deleteBeginTick = 0; + + if (considerNotes) + { + for (var i = 0; i < this.songData.notes.length; i++) + { + var note = this.songData.notes[i]; + if (note.tick + note.duration > lastTickToConsider) + continue; + + if (note.tick + note.duration == lastTickToConsider && + note.tick > deleteBeginTick) + { + deleteBeginTick = note.tick; + } + else if (note.tick + note.duration != lastTickToConsider && + note.tick + note.duration > deleteBeginTick) + { + deleteBeginTick = note.tick + note.duration; + } + } + } + + if (considerChords) + { + for (var i = 0; i < this.songData.chords.length; i++) + { + var chord = this.songData.chords[i]; + if (chord.tick + chord.duration > lastTickToConsider) + continue; + + if (chord.tick + chord.duration == lastTickToConsider && + chord.tick > deleteBeginTick) + { + deleteBeginTick = chord.tick; + } + else if (chord.tick + chord.duration != lastTickToConsider && + chord.tick + chord.duration > deleteBeginTick) + { + deleteBeginTick = chord.tick + chord.duration; + } + } + } + + if (considerNotes) + this.songData.removeNotesByTickRange(deleteBeginTick, lastTickToConsider, null); + + if (considerChords) + this.songData.removeChordsByTickRange(deleteBeginTick, lastTickToConsider); + + this.cursorTick = Math.max(0, deleteBeginTick); + this.clearHover(); + this.refreshRepresentation(); + this.refreshCanvas(); + this.callOnCursorChanged(); + } +} \ No newline at end of file diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 113f341..437d540 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -92,13 +92,6 @@ function isPointInside(p, rect) } -function transformMousePosition(elem, ev) -{ - var rect = elem.getBoundingClientRect(); - return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; -} - - SongEditor.prototype.getPositionForTick = function(tick) { for (var b = 0; b < this.viewBlocks.length; b++) @@ -115,31 +108,44 @@ SongEditor.prototype.getPositionForTick = function(tick) } -SongEditor.prototype.getTickAtPosition = function(x) +SongEditor.prototype.getRegionAtPosition = function(pos) { - for (var b = 0; b < this.viewBlocks.length; b++) + for (var r = 0; r < this.viewRegions.length; r++) { - var block = this.viewBlocks[b]; + var region = this.viewRegions[r]; - if (x >= block.x1 && - (b == this.viewBlocks.length - 1 || x <= this.viewBlocks[b + 1].x1)) - { - return Math.round((block.tick + Math.min(block.duration, Math.ceil((x - block.x1) / this.tickZoom))) / this.tickSnap) * this.tickSnap; - } + if (isPointInside(pos, region)) + return r; } - return 0; + return -1; } -SongEditor.prototype.getZoneAtPosition = function(y) +SongEditor.prototype.getTickAtPosition = function(pos) { - if (y >= this.MARGIN_TOP + this.HEADER_MARGIN && - y <= this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN) + var regionIndex = this.getRegionAtPosition(pos); + if (regionIndex < 0) + return -1; + + var region = this.viewRegions[regionIndex]; + return Math.round((region.tick + Math.min(region.duration, Math.ceil((pos.x - region.x1) / this.tickZoom))) / this.tickSnap) * this.tickSnap; +} + + +SongEditor.prototype.getZoneAtPosition = function(pos) +{ + var regionIndex = this.getRegionAtPosition(pos); + if (regionIndex < 0) + return this.CURSOR_ZONE_ALL; + + var region = this.viewRegions[regionIndex]; + if (pos.y >= region.y1 + this.HEADER_MARGIN && + pos.y <= region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) return this.CURSOR_ZONE_NOTES; - else if (y >= this.canvasHeight - this.MARGIN_BOTTOM - this.CHORD_HEIGHT && - y <= this.canvasHeight - this.MARGIN_BOTTOM) + else if (pos.y >= region.y2 - this.CHORD_HEIGHT && + pos.y <= region.y2) return this.CURSOR_ZONE_CHORDS; else @@ -253,16 +259,24 @@ SongEditor.prototype.getFirstAndLastScrollableRows = function() SongEditor.prototype.getNoteDragged = function(note, dragPosition) { - var dragTick = this.getTickAtPosition(dragPosition.x); - var pitchOffset = Math.round((this.mouseDragOrigin.y - dragPosition.y) / this.NOTE_HEIGHT * 2); - var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin.x); + var regionIndex = this.getRegionAtPosition(dragPosition); + var regionOriginIndex = this.getRegionAtPosition(this.mouseDragOrigin); + if (regionIndex == -1 || regionOriginIndex == -1) + return note; + + var region = this.viewRegions[regionIndex]; + var regionOrigin = this.viewRegions[regionOriginIndex]; + + var dragTick = this.getTickAtPosition(dragPosition); + var pitchOffset = Math.round((this.mouseDragOrigin.y - regionOrigin.y1 - (dragPosition.y - region.y1)) / this.NOTE_HEIGHT * 2); + var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin); if (this.mouseDragAction == "move") { return { tick: Math.max(0, note.tick + tickOffset), duration: note.duration, - pitch: Math.max(theory.getMinPitch(), Math.min(theory.getMaxPitch(), note.pitch + pitchOffset)) + pitch: Math.max(this.theory.getMinPitch(), Math.min(this.theory.getMaxPitch(), note.pitch + pitchOffset)) }; } else if (this.mouseDragAction == "stretch") @@ -351,549 +365,4 @@ SongEditor.prototype.getMeterChangeDragged = function(meterChange, dragPosition) // TODO: Should move proportionally, like notes. else if (this.mouseDragAction == "stretch") return { tick: meterChange.tick }; -} - - -SongEditor.prototype.handleMouseMove = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - if (!this.interactionEnabled) - return; - - this.canvas.style.cursor = "default"; - this.mouseDragCurrent = mousePos; - this.clearHover(); - - // Check what's under the mouse, if it's not down. - if (!this.mouseDown) - { - for (var b = 0; b < this.viewBlocks.length; b++) - { - if (isPointInside(mousePos, this.viewBlocks[b])) - { - // Check for notes. - for (var n = 0; n < this.viewBlocks[b].notes.length; n++) - { - var note = this.viewBlocks[b].notes[n]; - if (isPointInside(mousePos, note)) - { - this.hoverNote = note.noteIndex; - - if (mousePos.x <= note.resizeHandleL + this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchL = true; - } - else if (mousePos.x >= note.resizeHandleR - this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchR = true; - } - else - this.canvas.style.cursor = "pointer"; - - break; - } - } - - // Check for chords. - if (this.hoverNote < 0) - { - for (var n = 0; n < this.viewBlocks[b].chords.length; n++) - { - var chord = this.viewBlocks[b].chords[n]; - if (isPointInside(mousePos, chord)) - { - this.hoverChord = chord.chordIndex; - - if (mousePos.x <= chord.resizeHandleL + this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchL = true; - } - else if (mousePos.x >= chord.resizeHandleR - this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchR = true; - } - else - this.canvas.style.cursor = "pointer"; - - break; - } - } - } - - break; - } - } - - // Check for key changes. - if (this.hoverNote < 0 && this.hoverChord < 0) - { - for (var n = 0; n < this.viewKeyChanges.length; n++) - { - var keyChange = this.viewKeyChanges[n]; - if (isPointInside(mousePos, keyChange)) - { - this.hoverKeyChange = keyChange.keyChangeIndex; - this.canvas.style.cursor = "ew-resize"; - break; - } - } - } - - // Check for meter changes. - if (this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) - { - for (var n = 0; n < this.viewMeterChanges.length; n++) - { - var meterChange = this.viewMeterChanges[n]; - if (isPointInside(mousePos, meterChange)) - { - this.hoverMeterChange = meterChange.meterChangeIndex; - this.canvas.style.cursor = "ew-resize"; - break; - } - } - } - - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "move") - { - this.canvas.style.cursor = "move"; - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "stretch") - { - this.canvas.style.cursor = "ew-resize"; - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "scroll") - { - var rowOffset = (mousePos.y - this.mouseDragOrigin.y) / this.NOTE_HEIGHT; - this.rowAtCenter += rowOffset; - var rowLimits = this.getFirstAndLastScrollableRows(); - this.rowAtCenter = Math.min(rowLimits.first, Math.max(rowLimits.last, this.rowAtCenter)); - this.mouseDragOrigin.y = mousePos.y; - - var xOffset = (this.mouseDragOrigin.x - mousePos.x); - this.xAtLeft += xOffset; - this.xAtLeft = Math.max(0, Math.min(this.viewBlocks[this.viewBlocks.length - 1].x2 + this.canvasWidth / 2, this.xAtLeft)); - this.mouseDragOrigin.x = mousePos.x; - - this.refreshRepresentation(); - this.refreshCanvas(); - } -} - - -SongEditor.prototype.handleMouseDown = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - if (!this.interactionEnabled) - return; - - if (!ev.ctrlKey) - this.unselectAll(); - - this.mouseDragAction = null; - this.mouseDragOrigin = mousePos; - - this.cursorTick = this.getTickAtPosition(mousePos.x); - this.cursorZone = this.getZoneAtPosition(mousePos.y); - this.showCursor = true; - - // Start a dragging operation. - if (this.hoverNote >= 0) - { - this.showCursor = false; - if (!this.noteSelections[this.hoverNote]) - this.selectedObjects++; - - this.noteSelections[this.hoverNote] = true; - this.cursorTick = this.songData.notes[this.hoverNote].tick; - theory.playNoteSample(this.synth, this.songData.notes[this.hoverNote].pitch); - - if (this.hoverStretchR) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick + this.songData.notes[this.hoverNote].duration; - this.mouseStretchPivotTick = this.getEarliestSelectedTick(); - } - else if (this.hoverStretchL) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick; - this.mouseStretchPivotTick = this.getLatestSelectedTick(); - } - else - this.mouseDragAction = "move"; - } - else if (this.hoverChord >= 0) - { - this.showCursor = false; - if (!this.chordSelections[this.hoverChord]) - this.selectedObjects++; - - this.chordSelections[this.hoverChord] = true; - this.cursorTick = this.songData.chords[this.hoverChord].tick; - theory.playChordSample(this.synth, this.songData.chords[this.hoverChord].chord, this.songData.chords[this.hoverChord].rootPitch); - - if (this.hoverStretchR) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick + this.songData.chords[this.hoverChord].duration; - this.mouseStretchPivotTick = this.getEarliestSelectedTick(); - } - else if (this.hoverStretchL) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick; - this.mouseStretchPivotTick = this.getLatestSelectedTick(); - } - else - this.mouseDragAction = "move"; - } - else if (this.hoverKeyChange >= 0) - { - if (!this.keyChangeSelections[this.hoverKeyChange]) - this.selectedObjects++; - - this.showCursor = false; - this.keyChangeSelections[this.hoverKeyChange] = true; - this.mouseDragAction = "move"; - } - else if (this.hoverMeterChange >= 0) - { - if (!this.meterChangeSelections[this.hoverMeterChange]) - this.selectedObjects++; - - this.showCursor = false; - this.meterChangeSelections[this.hoverMeterChange] = true; - this.mouseDragAction = "move"; - } - else - { - this.mouseDragAction = "scroll"; - this.clearHover(); - } - - this.mouseDown = true; - this.callOnSelectionChanged(); - this.refreshCanvas(); - this.callOnCursorChanged(); -} - - -SongEditor.prototype.handleMouseUp = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - if (!this.interactionEnabled) - return; - - // Apply dragged modifications. - if (this.mouseDown && this.mouseDragAction != null && this.mouseDragAction != "scroll") - { - // Store modified objects in a local array and - // remove them from the song data. - var selectedNotes = []; - for (var n = this.noteSelections.length - 1; n >= 0; n--) - { - if (this.noteSelections[n]) - { - selectedNotes.push(this.songData.notes[n]); - this.songData.notes.splice(n, 1); - } - } - - var selectedChords = []; - for (var n = this.chordSelections.length - 1; n >= 0; n--) - { - if (this.chordSelections[n]) - { - selectedChords.push(this.songData.chords[n]); - this.songData.chords.splice(n, 1); - } - } - - var selectedKeyChanges = []; - for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) - { - if (this.keyChangeSelections[n]) - { - selectedKeyChanges.push(this.songData.keyChanges[n]); - this.songData.keyChanges.splice(n, 1); - } - } - - var selectedMeterChanges = []; - for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) - { - if (this.meterChangeSelections[n]) - { - selectedMeterChanges.push(this.songData.meterChanges[n]); - this.songData.meterChanges.splice(n, 1); - } - } - - // Apply the modification and reinsert objects into the song data. - var newNotes = []; - for (var n = 0; n < selectedNotes.length; n++) - { - var draggedNote = this.getNoteDragged(selectedNotes[n], mousePos); - var newNote = new SongDataNote(draggedNote.tick, draggedNote.duration, draggedNote.pitch); - newNotes.push(newNote); - this.songData.addNote(newNote); - } - - var newChords = []; - for (var n = 0; n < selectedChords.length; n++) - { - var draggedChord = this.getChordDragged(selectedChords[n], mousePos); - var newChord = new SongDataChord(draggedChord.tick, draggedChord.duration, selectedChords[n].chord, selectedChords[n].rootPitch); - newChords.push(newChord); - this.songData.addChord(newChord); - } - - var newKeyChanges = []; - for (var n = 0; n < selectedKeyChanges.length; n++) - { - var draggedKeyChange = this.getKeyChangeDragged(selectedKeyChanges[n], mousePos); - var newKeyChange = new SongDataKeyChange(draggedKeyChange.tick, selectedKeyChanges[n].scale, selectedKeyChanges[n].tonicPitch); - newKeyChanges.push(newKeyChange); - this.songData.addKeyChange(newKeyChange); - } - - var newMeterChanges = []; - for (var n = 0; n < selectedMeterChanges.length; n++) - { - var draggedMeterChange = this.getMeterChangeDragged(selectedMeterChanges[n], mousePos); - var newMeterChange = new SongDataMeterChange(draggedMeterChange.tick, selectedMeterChanges[n].numerator, selectedMeterChanges[n].denominator); - newMeterChanges.push(newMeterChange); - this.songData.addMeterChange(newMeterChange); - } - - this.clearHover(); - this.refreshRepresentation(); - - // Find which objects were selected before, and select them again. - for (var n = 0; n < this.songData.notes.length; n++) - { - for (var m = 0; m < newNotes.length; m++) - { - if (this.songData.notes[n] == newNotes[m]) - { - this.selectedObjects++; - this.noteSelections[n] = true; - } - } - } - - for (var n = 0; n < this.songData.chords.length; n++) - { - for (var m = 0; m < newChords.length; m++) - { - if (this.songData.chords[n] == newChords[m]) - { - this.selectedObjects++; - this.chordSelections[n] = true; - } - } - } - - for (var n = 0; n < this.songData.keyChanges.length; n++) - { - for (var m = 0; m < newKeyChanges.length; m++) - { - if (this.songData.keyChanges[n] == newKeyChanges[m]) - { - this.selectedObjects++; - this.keyChangeSelections[n] = true; - } - } - } - - for (var n = 0; n < this.songData.meterChanges.length; n++) - { - for (var m = 0; m < newMeterChanges.length; m++) - { - if (this.songData.meterChanges[n] == newMeterChanges[m]) - { - this.selectedObjects++; - this.meterChangeSelections[n] = true; - } - } - } - } - - this.mouseDown = false; - this.mouseDragAction = null; - this.refreshCanvas(); -} - - -SongEditor.prototype.handleKeyDown = function(ev) -{ - if (!this.interactionEnabled) - return; - - var keyCode = (ev.key || ev.which || ev.keyCode); - - if (keyCode == 46 || (keyCode == 8 && this.selectedObjects > 0)) // Delete/Backspace - { - ev.preventDefault(); - - // Remove selected objects from the song data. - var selectedNotes = []; - for (var n = this.noteSelections.length - 1; n >= 0; n--) - { - if (this.noteSelections[n]) - { - selectedNotes.push(this.songData.notes[n]); - this.songData.notes.splice(n, 1); - } - } - - var selectedChords = []; - for (var n = this.chordSelections.length - 1; n >= 0; n--) - { - if (this.chordSelections[n]) - { - selectedChords.push(this.songData.chords[n]); - this.songData.chords.splice(n, 1); - } - } - - var selectedKeyChanges = []; - for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) - { - if (this.keyChangeSelections[n]) - { - selectedKeyChanges.push(this.songData.keyChanges[n]); - this.songData.keyChanges.splice(n, 1); - } - } - - var selectedMeterChanges = []; - for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) - { - if (this.meterChangeSelections[n]) - { - selectedMeterChanges.push(this.songData.meterChanges[n]); - this.songData.meterChanges.splice(n, 1); - } - } - - this.showCursor = true; - this.clearHover(); - this.refreshRepresentation(); - this.refreshCanvas(); - this.callOnSelectionChanged(); - } - - else if (keyCode == 8 && this.selectedObjects == 0) // Backspace - { - ev.preventDefault(); - - var lastTickToConsider = 0; - var considerNotes = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_NOTES); - var considerChords = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_CHORDS); - - if (considerNotes) - { - for (var i = 0; i < this.songData.notes.length; i++) - { - var note = this.songData.notes[i]; - if (note.tick + note.duration > lastTickToConsider) - lastTickToConsider = Math.min(note.tick + note.duration, this.cursorTick); - } - } - - if (considerChords) - { - for (var i = 0; i < this.songData.chords.length; i++) - { - var chord = this.songData.chords[i]; - if (chord.tick + chord.duration > lastTickToConsider) - lastTickToConsider = Math.min(chord.tick + chord.duration, this.cursorTick); - } - } - - if (lastTickToConsider != this.cursorTick) - { - this.cursorTick = Math.max(0, lastTickToConsider); - - this.clearHover(); - this.refreshCanvas(); - this.callOnCursorChanged(); - return; - } - - var deleteBeginTick = 0; - - if (considerNotes) - { - for (var i = 0; i < this.songData.notes.length; i++) - { - var note = this.songData.notes[i]; - if (note.tick + note.duration > lastTickToConsider) - continue; - - if (note.tick + note.duration == lastTickToConsider && - note.tick > deleteBeginTick) - { - deleteBeginTick = note.tick; - } - else if (note.tick + note.duration != lastTickToConsider && - note.tick + note.duration > deleteBeginTick) - { - deleteBeginTick = note.tick + note.duration; - } - } - } - - if (considerChords) - { - for (var i = 0; i < this.songData.chords.length; i++) - { - var chord = this.songData.chords[i]; - if (chord.tick + chord.duration > lastTickToConsider) - continue; - - if (chord.tick + chord.duration == lastTickToConsider && - chord.tick > deleteBeginTick) - { - deleteBeginTick = chord.tick; - } - else if (chord.tick + chord.duration != lastTickToConsider && - chord.tick + chord.duration > deleteBeginTick) - { - deleteBeginTick = chord.tick + chord.duration; - } - } - } - - if (considerNotes) - this.songData.removeNotesByTickRange(deleteBeginTick, lastTickToConsider, null); - - if (considerChords) - this.songData.removeChordsByTickRange(deleteBeginTick, lastTickToConsider); - - this.cursorTick = Math.max(0, deleteBeginTick); - this.clearHover(); - this.refreshRepresentation(); - this.refreshCanvas(); - this.callOnCursorChanged(); - } } \ No newline at end of file diff --git a/src/editor_representation.js b/src/editor_representation.js index f5ebc1f..99f8f57 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -3,11 +3,7 @@ SongEditor.prototype.refreshRepresentation = function() { // Clear representation objects. - this.viewBlocks = []; - this.viewNotes = []; - this.viewChords = []; - this.viewKeyChanges = []; - this.viewMeterChanges = []; + this.viewRegions = []; // Clear selection arrays and push a boolean false for each object in the song data, // effectively unselecting everything, while also accomodating added/removed objects. @@ -29,237 +25,128 @@ SongEditor.prototype.refreshRepresentation = function() for (var i = 0; i < this.songData.meterChanges.length; i++) this.meterChangeSelections.push(false); - // Set up some layout constants. - var blockY1 = this.MARGIN_TOP + this.HEADER_MARGIN; - var blockY2 = this.canvasHeight - this.MARGIN_BOTTOM; - var changeY1 = this.MARGIN_TOP; - var chordY2 = this.canvasHeight - this.MARGIN_BOTTOM; - - // Set up current block drawing position, current tick, and - // iterators through the song data. - var x = this.MARGIN_LEFT - this.xAtLeft; - var tick = 0; - var curNote = 0; - var curChord = 0; - var curKeyChange = 0; - var curMeterChange = 0; - - // Set up the first block. - var curBlock = 0; - this.viewBlocks.push( - { - tick: 0, - duration: 0, - key: new SongDataKeyChange(0, theory.scales[0], 0), - meter: new SongDataMeterChange(0, 4, 4), - notes: [], - chords: [], - x1: x, - y1: blockY1, - x2: x, - y2: blockY2 - }); - - // Loop will break after the last block. - while (true) + // Iterate through song sections. + var currentY = this.MARGIN_TOP; + var currentKeyChangeIndex = 0; + var currentMeterChangeIndex = 0; + for (var i = 0; i < this.songData.sectionBreaks.length + 1; i++) { - // Identifiers for whether there's a following key or meter change. - var NEXT_IS_NONE = 0; - var NEXT_IS_KEYCHANGE = 1; - var NEXT_IS_METERCHANGE = 2; + var sectionTickRange = this.songData.getSectionTickRange(i); + var sectionDuration = sectionTickRange.end - sectionTickRange.start; - // Find the tick where the current block ends due to a key or meter change. - var nextChangeTick = this.songData.lastTick; - var nextIsWhat = NEXT_IS_NONE; + var lowestNoteRow = 7 * 5; + var highestNoteRow = 7 * 6; - if (curKeyChange < this.songData.keyChanges.length) + // Create regions with non-changing key and meter. + var regions = []; + var currentRegionStart = sectionTickRange.start; + while (currentRegionStart < sectionTickRange.end) { - var nextKeyChange = this.songData.keyChanges[curKeyChange] - if (nextKeyChange.tick < nextChangeTick) - { - nextChangeTick = nextKeyChange.tick; - nextIsWhat = NEXT_IS_KEYCHANGE; - } - } - - if (curMeterChange < this.songData.meterChanges.length) - { - var nextMeterChange = this.songData.meterChanges[curMeterChange] - if (nextMeterChange.tick < nextChangeTick) + // Find next key change tick and next meter change tick. + var nextKeyChangeTick = sectionTickRange.end; + var nextMeterChangeTick = sectionTickRange.end; + var willChangeKey = false; + var willChangeMeter = false; + + if (currentKeyChangeIndex + 1 < this.songData.keyChanges.length && + this.songData.keyChanges[currentKeyChangeIndex + 1].tick < nextKeyChangeTick) { - nextChangeTick = nextMeterChange.tick; - nextIsWhat = NEXT_IS_METERCHANGE; + nextKeyChangeTick = this.songData.keyChanges[currentKeyChangeIndex + 1].tick; + willChangeKey = true; } - } - - // Advance draw position until the block's end tick. - x += (nextChangeTick - tick) * this.tickZoom; - tick = nextChangeTick; - - // If there is a key change, add its representation and advance its iterator. - var blockX2 = x; - - if (nextIsWhat == NEXT_IS_KEYCHANGE) - { - x += this.KEYCHANGE_BAR_WIDTH; - this.viewKeyChanges.push( + if (currentMeterChangeIndex + 1 < this.songData.meterChanges.length && + this.songData.meterChanges[currentMeterChangeIndex + 1].tick < nextMeterChangeTick) { - keyChangeIndex: curKeyChange, - tick: nextChangeTick, - x1: blockX2, - y1: changeY1, - x2: x, - y2: chordY2 - }); - - curKeyChange++; - } - // Or if there is a meter change, add its representation and advance its iterator. - else if (nextIsWhat == NEXT_IS_METERCHANGE) - { - x += this.METERCHANGE_BAR_WIDTH; + nextMeterChangeTick = this.songData.meterChanges[currentMeterChangeIndex + 1].tick; + willChangeMeter = true; + } - this.viewMeterChanges.push( - { - meterChangeIndex: curMeterChange, - tick: nextChangeTick, - x1: blockX2, - y1: changeY1 + 20, - x2: x, - y2: chordY2 - }); + var regionEndTick = Math.min(nextKeyChangeTick, nextMeterChangeTick); + var keyChange = this.songData.keyChanges[currentKeyChangeIndex]; + var meterChange = this.songData.meterChanges[currentMeterChangeIndex]; - curMeterChange++; - } - - // Then finish off the current block of notes. - // If its duration would be zero, ignore it for now but prepare it for the next iteration. - var block = this.viewBlocks[curBlock]; - - if (nextChangeTick == block.tick) - { - block.x1 = x; - block.x2 = x; - } - else - { - block.duration = nextChangeTick - block.tick; - block.x2 = blockX2; - - // Add notes' representations. + // Find lowest and highest pitch in the section. + var notes = []; for (var n = 0; n < this.songData.notes.length; n++) { var note = this.songData.notes[n]; - if (note.tick + note.duration <= block.tick || note.tick >= block.tick + block.duration) - continue; - - var noteRow = theory.getRowForPitch(note.pitch, block.key.scale, block.key.tonicPitch); - var notePos = this.getNotePosition(block, noteRow, note.tick, note.duration); - block.notes.push( + if (note.tick < sectionTickRange.end && + note.tick + note.duration >= sectionTickRange.start) { - noteIndex: n, - tick: note.tick, - duration: note.duration, - resizeHandleL: notePos.resizeHandleL, - resizeHandleR: notePos.resizeHandleR, - x1: notePos.x1, - y1: notePos.y1, - x2: notePos.x2, - y2: notePos.y2 - }); + var noteRow = this.theory.getRowForPitch(note.pitch, keyChange.scale, keyChange.tonicPitch); + lowestNoteRow = Math.min(lowestNoteRow, Math.floor(noteRow)); + highestNoteRow = Math.max(highestNoteRow, Math.ceil(noteRow) + 1); + notes.push(n); + } } - // Add chords' representations. - for (var n = 0; n < this.songData.chords.length; n++) - { - var chord = this.songData.chords[n]; - - if (chord.tick + chord.duration <= block.tick || chord.tick >= block.tick + block.duration) - continue; - - var chordPos = this.getChordPosition(block, chord.tick, chord.duration); - block.chords.push( - { - chordIndex: n, - tick: chord.tick, - duration: chord.duration, - resizeHandleL: chordPos.resizeHandleL, - resizeHandleR: chordPos.resizeHandleR, - x1: chordPos.x1, - y1: chordPos.y1, - x2: chordPos.x2, - y2: chordPos.y2 - }); - } + // Add region to list. + var region = { + tick: currentRegionStart, + duration: regionEndTick - currentRegionStart, + x1: this.MARGIN_LEFT + (currentRegionStart - sectionTickRange.start) * this.tickZoom, + y1: 0, + x2: this.MARGIN_LEFT + (regionEndTick - sectionTickRange.start) * this.tickZoom, + y2: 0, + showKeyChange: this.songData.keyChanges[currentKeyChangeIndex].tick == currentRegionStart, + showMeterChange: this.songData.meterChanges[currentMeterChangeIndex].tick == currentRegionStart, + key: this.songData.keyChanges[currentKeyChangeIndex], + meter: this.songData.meterChanges[currentMeterChangeIndex], + notes: notes + }; - // If this is the final block, we can stop now. - if (nextIsWhat == NEXT_IS_NONE) - break; + regions.push(region); + this.viewRegions.push(region); - // Or else, add a new block for the next iteration. - this.viewBlocks.push( - { - tick: tick, - duration: 0, - key: block.key, - meter: block.meter, - notes: [], - chords: [], - x1: x, - y1: blockY1, - x2: x, - y2: blockY2 - }); + currentRegionStart = regionEndTick; - curBlock++; + if (nextKeyChangeTick == nextMeterChangeTick && willChangeKey && willChangeMeter) + { + currentKeyChangeIndex++; + currentMeterChangeIndex++; + } + else if (nextKeyChangeTick < nextMeterChangeTick && willChangeKey) + { + currentKeyChangeIndex++; + } + else if (nextMeterChangeTick < nextKeyChangeTick && willChangeMeter) + { + currentMeterChangeIndex++; + } } - // Apply key/meter changes to the next block. - if (nextIsWhat == NEXT_IS_KEYCHANGE) + // Add the section representation. + var sectionHeight = + this.HEADER_HEIGHT + + (highestNoteRow - lowestNoteRow) * this.NOTE_HEIGHT + + this.CHORD_NOTE_SEPARATION + this.CHORD_HEIGHT; + + for (var r = 0; r < regions.length; r++) { - this.viewKeyChanges[this.viewKeyChanges.length - 1].blockIndex = curBlock; - this.viewBlocks[curBlock].key = this.songData.keyChanges[curKeyChange - 1]; + regions[r].lowestNoteRow = lowestNoteRow; + regions[r].highestNoteRow = highestNoteRow; + regions[r].y1 = currentY; + regions[r].y2 = currentY + sectionHeight; } - else if (nextIsWhat == NEXT_IS_METERCHANGE) - { - this.viewMeterChanges[this.viewMeterChanges.length - 1].blockIndex = curBlock; - this.viewBlocks[curBlock].meter = this.songData.meterChanges[curMeterChange - 1]; - } + currentY += sectionHeight + this.SECTION_SEPARATION; } + + this.canvas.height = currentY; } -// Returns the bounds of the given row's note's representation rectangle. -SongEditor.prototype.getNotePosition = function(block, row, tick, duration) -{ - var blockTick = tick - block.tick; - var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; - var firstRow = 7 * 3; - var rowY = this.getYForRow(block, row); - return { - resizeHandleL: block.x1 + blockTick * this.tickZoom, - resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, - x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), - x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), - y1: rowY - this.NOTE_HEIGHT + this.NOTE_MARGIN_VER, - y2: rowY - this.NOTE_MARGIN_VER - }; -} - - -// Returns the bounds of the given chord's representation rectangle. -SongEditor.prototype.getChordPosition = function(block, tick, duration) +SongEditor.prototype.getNotePosition = function(region, row, tick, duration) { - var blockTick = tick - block.tick; + var x1 = region.x1 + (tick - region.tick) * this.tickZoom; + var y1 = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - (row - region.lowestNoteRow + 1) * this.NOTE_HEIGHT; + return { - resizeHandleL: block.x1 + blockTick * this.tickZoom, - resizeHandleR: block.x1 + (blockTick + duration) * this.tickZoom, - x1: block.x1 + Math.max(0, (blockTick * this.tickZoom) + this.NOTE_MARGIN_HOR), - x2: block.x1 + Math.min(block.x2 - block.x1, (blockTick + duration) * this.tickZoom - this.NOTE_MARGIN_HOR), - y1: block.y2 - this.CHORD_HEIGHT, - y2: block.y2 + x1: x1, + y1: y1, + x2: x1 + duration * this.tickZoom, + y2: y1 + this.NOTE_HEIGHT }; } \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js index 82acef1..1393ae3 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -1,5 +1,6 @@ -function SongData() +function SongData(theory) { + this.theory = theory; this.clear(); } @@ -9,6 +10,8 @@ function SongDataNote(tick, duration, pitch) this.tick = tick; this.duration = duration; this.pitch = pitch; + this.startSection = -1; + this.endSection = -1; } @@ -18,6 +21,8 @@ function SongDataChord(tick, duration, chord, rootPitch) this.duration = duration; this.chord = chord; this.rootPitch = rootPitch; + this.startSection = -1; + this.endSection = -1; } @@ -26,6 +31,7 @@ function SongDataKeyChange(tick, scale, tonicPitch) this.tick = tick; this.scale = scale; this.tonicPitch = tonicPitch; + this.section = -1; } @@ -34,6 +40,13 @@ function SongDataMeterChange(tick, numerator, denominator) this.tick = tick; this.numerator = numerator; this.denominator = denominator; + this.section = -1; +} + + +function SongSectionBreak(tick) +{ + this.tick = tick; } @@ -59,14 +72,15 @@ function arrayAddSortedByTick(arr, obj) SongData.prototype.clear = function() { this.beatsPerMinute = 120; - this.ticksPerBeat = 960; - this.lastTick = 9600; + this.ticksPerWholeNote = 960; + this.endTick = this.ticksPerWholeNote * 4; this.notes = []; this.chords = []; this.keyChanges = []; this.meterChanges = []; + this.sectionBreaks = []; - this.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); + this.addKeyChange(new SongDataKeyChange(0, this.theory.scales[0], this.theory.C)); this.addMeterChange(new SongDataMeterChange(0, 4, 4)); } @@ -90,7 +104,7 @@ SongData.prototype.isValidDuration = function(duration) // Returns whether the given pitch value is valid. SongData.prototype.isValidPitch = function(pitch) { - return (pitch >= theory.getMinPitch() && pitch <= theory.getMaxPitch() && (pitch % 1) == 0) + return (pitch >= this.theory.getMinPitch() && pitch <= this.theory.getMaxPitch() && (pitch % 1) == 0) } @@ -160,6 +174,26 @@ SongData.prototype.addMeterChange = function(meterChange) } +// Adds a section break to the data, and returns whether it was successful. +SongData.prototype.addSectionBreak = function(sectionBreak) +{ + if (!this.isValidTick(sectionBreak.tick) || sectionBreak.tick == 0 || sectionBreak.tick == this.endTick) + return false; + + // Remove meter changes which were at the same tick. + for (var i = this.sectionBreaks.length - 1; i >= 0; i--) + { + if (this.sectionBreaks[i].tick == sectionBreak.tick) + { + this.sectionBreaks.splice(i, 1); + } + } + + arrayAddSortedByTick(this.sectionBreaks, sectionBreak); + return true; +} + + // Remove or truncate notes that fall between tickBegin and tickEnd, optionally only if // their pitches match the given pitch. Pass null for pitch to not consider pitches. SongData.prototype.removeNotesByTickRange = function(tickBegin, tickEnd, pitch) @@ -214,6 +248,21 @@ SongData.prototype.removeChordsByTickRange = function(tickBegin, tickEnd) } +// Gets the start and end ticks for the given section index. +SongData.prototype.getSectionTickRange = function(sectionIndex) +{ + var startTick = 0; + if (sectionIndex - 1 >= 0) + startTick = this.sectionBreaks[sectionIndex - 1].tick; + + var endTick = this.endTick; + if (sectionIndex < this.sectionBreaks.length) + endTick = this.sectionBreaks[sectionIndex].tick; + + return { start: startTick, end: endTick }; +} + + // Returns a JSON string containing the song data. SongData.prototype.save = function() { diff --git a/src/theory.js b/src/theory.js index 960f81b..e91ec6f 100644 --- a/src/theory.js +++ b/src/theory.js @@ -1,8 +1,7 @@ -var theory = new Theory(); - - function Theory() { + var that = this; + var C = 0; var Cs = 1; var D = 2; @@ -203,8 +202,8 @@ function Theory() // indicates that the pitch falls between scale degrees. this.getRowForPitch = function(pitch, scale, tonicPitch) { - var pitchOctave = theory.getOctaveForPitch(pitch - tonicPitch); - var pitchDegree = theory.getDegreeForPitch(pitch, scale, tonicPitch); + var pitchOctave = that.getOctaveForPitch(pitch - tonicPitch); + var pitchDegree = that.getDegreeForPitch(pitch, scale, tonicPitch); var offset = 0; if (tonicPitch != 0) @@ -222,7 +221,7 @@ function Theory() var offset = 0; if (tonicPitch != 0) - offset = Math.floor(theory.getRowForPitch(tonicPitch, scale, 0)); + offset = Math.floor(that.getRowForPitch(tonicPitch, scale, 0)); return scale.pitches[Math.floor(row + 7 - offset) % 7] + tonicPitch + Math.floor((row - offset) / 7) * 12; }; From 4e197864d26eb4bebee0564a1f009a41c3ac9ab1 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 24 Apr 2016 16:41:27 -0300 Subject: [PATCH 23/46] WIP: fix some editor features --- main.js | 4 + src/editor.js | 1 + src/editor_drawing.js | 295 ++++++++++++++++++++--------------- src/editor_event_handling.js | 86 +++++----- src/editor_interaction.js | 18 +-- src/editor_representation.js | 98 ++++++++++-- 6 files changed, 318 insertions(+), 184 deletions(-) diff --git a/main.js b/main.js index e283df7..c8737f5 100644 --- a/main.js +++ b/main.js @@ -10,6 +10,10 @@ function testOnLoad() song.addMeterChange(new SongDataMeterChange(960 * 2, 3, 4)); song.addSectionBreak(new SongSectionBreak(960 * 1.5)); + song.addChord(new SongDataChord(960 * 0.0, 960 / 2, theory.chords[0], theory.C)); + song.addChord(new SongDataChord(960 * 0.5, 960 / 2, theory.chords[0], theory.F)); + song.addChord(new SongDataChord(960 * 1.0, 960 / 1, theory.chords[0], theory.G)); + song.addNote(new SongDataNote(0, 960 / 4, theory.C + 60)); song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.D + 60)); song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); diff --git a/src/editor.js b/src/editor.js index 75351c1..5769904 100644 --- a/src/editor.js +++ b/src/editor.js @@ -71,6 +71,7 @@ function SongEditor(canvas, theory, synth, songData) this.MARGIN_BOTTOM = 8; this.SECTION_SEPARATION = 10; this.HEADER_HEIGHT = 40; + this.HEADER_LINE_HEIGHT = 16; this.NOTE_HEIGHT = 14; this.NOTE_MARGIN_HOR = 0.5; this.NOTE_MARGIN_VER = 0.5; diff --git a/src/editor_drawing.js b/src/editor_drawing.js index 2d799de..e97d360 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -1,5 +1,8 @@ SongEditor.prototype.refreshCanvas = function() { + this.canvasWidth = parseFloat(this.canvas.width); + this.canvasHeight = parseFloat(this.canvas.height); + this.ctx.save(); @@ -14,8 +17,6 @@ SongEditor.prototype.refreshCanvas = function() var NOTE_LINE_COLOR = "#dddddd"; var MEASURE_COLOR = "#dddddd"; var MEASURE_COLOR_STRONG = "#888888"; - var KEY_CHANGE_COLOR = "#aaaaaa"; - var METER_CHANGE_COLOR = "#88aaaa"; this.ctx.save(); this.ctx.beginPath(); @@ -49,8 +50,8 @@ SongEditor.prototype.refreshCanvas = function() for (var n = 1; n < region.highestNoteRow - region.lowestNoteRow; n++) { var y = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - n * this.NOTE_HEIGHT; - this.ctx.moveTo(region.x1, y); - this.ctx.lineTo(region.x2, y); + this.ctx.moveTo(region.x1 + 1, y); + this.ctx.lineTo(region.x2 - 1, y); } this.ctx.stroke(); @@ -64,22 +65,30 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.beginPath(); this.ctx.moveTo( region.x1 + n * this.tickZoom, - region.y1 + this.HEADER_HEIGHT); + region.y1 + this.HEADER_HEIGHT + 1); this.ctx.lineTo( region.x1 + n * this.tickZoom, - region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION); + region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - 1); this.ctx.moveTo( region.x1 + n * this.tickZoom, - region.y2 - this.CHORD_HEIGHT); + region.y2 - this.CHORD_HEIGHT + 1); this.ctx.lineTo( region.x1 + n * this.tickZoom, - region.y2); + region.y2 - 1); this.ctx.stroke(); } beatCount = (beatCount + 1) % region.meter.numerator; } + this.ctx.save(); + this.ctx.rect( + region.x1, + region.y1 + this.HEADER_HEIGHT, + region.x2 - region.x1, + (region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - (region.y1 + this.HEADER_HEIGHT)); + this.ctx.clip(); + // Draw notes. for (var n = 0; n < region.notes.length; n++) { @@ -104,139 +113,98 @@ SongEditor.prototype.refreshCanvas = function() } } - // Draw key change. - if (region.showKeyChange) - { - this.ctx.strokeStyle = KEY_CHANGE_COLOR; - this.ctx.beginPath(); - this.ctx.moveTo(region.x1, region.y1); - this.ctx.lineTo(region.x1, region.y2); - this.ctx.stroke(); - - this.ctx.font = "14px Tahoma"; - this.ctx.fillStyle = KEY_CHANGE_COLOR; - this.ctx.fillText( - this.theory.getNameForPitch(region.key.tonicPitch, region.key.scale, region.key.tonicPitch) + " " + region.key.scale.name, - region.x1 + 8, - region.y1 + 12); - } + this.ctx.restore(); - // Draw meter change. - if (region.showMeterChange) + // Draw chords. + for (var n = 0; n < region.chords.length; n++) { - this.ctx.strokeStyle = METER_CHANGE_COLOR; - this.ctx.beginPath(); - this.ctx.moveTo(region.x1, region.y1 + 20); - this.ctx.lineTo(region.x1, region.y2); - this.ctx.stroke(); + var chordIndex = region.chords[n]; + var chord = this.songData.chords[chordIndex]; - this.ctx.font = "14px Tahoma"; - this.ctx.fillStyle = METER_CHANGE_COLOR; - this.ctx.fillText( - "" + region.meter.numerator + " / " + region.meter.denominator, - region.x1 + 8, - region.y1 + 32); + if (!this.chordSelections[chordIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") + this.drawChord(region, chord, chord.tick, chord.duration, chordIndex == this.hoverChord, this.chordSelections[chordIndex]); } - } - - /*// Draw note measures. - var submeasureCount = 0; - for (var n = block.meter.tick - block.tick; n < block.duration; n += this.WHOLE_NOTE_DURATION / block.meter.denominator) + + // Draw dragged chords. + if (this.mouseDragAction != null && this.mouseDragAction != "scroll") { - if (n >= 0) + for (var n = 0; n < this.songData.chords.length; n++) { - this.ctx.strokeStyle = (submeasureCount == 0 ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y1); - this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN); - this.ctx.stroke(); + if (!this.chordSelections[n]) + continue; + + var chord = this.songData.chords[n]; + var chordDragged = this.getChordDragged(chord, this.mouseDragCurrent); + this.drawChord(region, chord, chordDragged.tick, chordDragged.duration, chordIndex == this.hoverChord, true); } - - submeasureCount = (submeasureCount + 1) % block.meter.numerator; } - // Draw notes. - for (var n = 0; n < block.notes.length; n++) + // Draw key change. + for (var n = 0; n < region.keyChanges.length; n++) { - var noteIndex = block.notes[n].noteIndex; - var note = this.songData.notes[noteIndex]; + var keyChangeIndex = region.keyChanges[n]; + var keyChange = this.songData.keyChanges[keyChangeIndex]; - if (!this.noteSelections[noteIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") - this.drawNote(i, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); + if (!this.keyChangeSelections[keyChangeIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") + this.drawKeyChange(region, keyChange, keyChange.tick, keyChangeIndex == this.hoverKeyChange, this.keyChangeSelections[keyChangeIndex]); } + // Draw dragged key changes. if (this.mouseDragAction != null && this.mouseDragAction != "scroll") { - for (var n = 0; n < this.songData.notes.length; n++) - { - var note = this.songData.notes[n]; - if (this.noteSelections[n]) - { - var draggedNote = this.getNoteDragged(note, this.mouseDragCurrent); - this.drawNote(i, draggedNote.pitch, draggedNote.tick, draggedNote.duration, noteIndex == this.hoverNote, true); - } - } - } - - this.ctx.restore(); - - // Draw chord measures. - var submeasureCount = 0; - for (var n = block.meter.tick - block.tick; n < block.duration; n += this.WHOLE_NOTE_DURATION / block.meter.denominator) - { - if (n >= 0) + for (var n = 0; n < this.songData.keyChanges.length; n++) { - this.ctx.strokeStyle = (submeasureCount == 0 ? BLOCK_MEASURE_COLOR_STRONG : BLOCK_MEASURE_COLOR); - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - this.ctx.moveTo(block.x1 + n * this.tickZoom, block.y2 - this.CHORD_HEIGHT); - this.ctx.lineTo(block.x1 + n * this.tickZoom, block.y2); - this.ctx.stroke(); + if (!this.keyChangeSelections[n]) + continue; + + var keyChange = this.songData.keyChanges[n]; + var keyChangeDragged = this.getKeyChangeDragged(keyChange, this.mouseDragCurrent); + + this.drawKeyChange(region, keyChange, keyChangeDragged.tick, keyChangeIndex == this.hoverKeyChange, true); } - - submeasureCount = (submeasureCount + 1) % block.meter.numerator; } - // Draw chords. - for (var n = 0; n < block.chords.length; n++) + // Draw meter change. + for (var n = 0; n < region.meterChanges.length; n++) { - var chordIndex = block.chords[n].chordIndex; - var chord = this.songData.chords[chordIndex]; + var meterChangeIndex = region.meterChanges[n]; + var meterChange = this.songData.meterChanges[meterChangeIndex]; - if (!this.chordSelections[chordIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") - this.drawChord(i, chord, chord.tick, chord.duration, chordIndex == this.hoverChord, this.chordSelections[chordIndex]); + if (!this.meterChangeSelections[meterChangeIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") + this.drawMeterChange(region, meterChange, meterChange.tick, meterChangeIndex == this.hoverMeterChange, this.meterChangeSelections[meterChangeIndex]); } + // Draw dragged meter changes. if (this.mouseDragAction != null && this.mouseDragAction != "scroll") { - for (var n = 0; n < this.songData.chords.length; n++) + for (var n = 0; n < this.songData.meterChanges.length; n++) { - var chord = this.songData.chords[n]; - if (this.chordSelections[n]) - { - var draggedChord = this.getChordDragged(chord, this.mouseDragCurrent); - this.drawChord(i, chord, draggedChord.tick, draggedChord.duration, chordIndex == this.hoverChord, true); - } + if (!this.meterChangeSelections[n]) + continue; + + var meterChange = this.songData.meterChanges[n]; + var meterChangeDragged = this.getMeterChangeDragged(meterChange, this.mouseDragCurrent); + + this.drawMeterChange(region, meterChange, meterChangeDragged.tick, meterChangeIndex == this.hoverMeterChange, true); } } // Draw cursor. - if (this.showCursor && this.cursorTick >= block.tick && this.cursorTick < block.tick + block.duration) + if (this.showCursor && this.cursorTick >= region.tick && this.cursorTick <= region.tick + region.duration) { this.ctx.strokeStyle = CURSOR_COLOR; this.ctx.fillStyle = CURSOR_COLOR; this.ctx.lineWidth = 2; - var cursorX = block.x1 + (this.cursorTick - block.tick) * this.tickZoom; - var cursorY1 = block.y1; - var cursorY2 = block.y2; + var cursorX = region.x1 + (this.cursorTick - region.tick) * this.tickZoom; + var cursorY1 = region.y1 + this.HEADER_HEIGHT; + var cursorY2 = region.y2; if (this.cursorZone == this.CURSOR_ZONE_NOTES) - cursorY2 = block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; + cursorY2 = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION; else if (this.cursorZone == this.CURSOR_ZONE_CHORDS) - cursorY1 = block.y2 - this.CHORD_HEIGHT; + cursorY1 = region.y2 - this.CHORD_HEIGHT; this.ctx.beginPath(); this.ctx.moveTo(cursorX, cursorY1); @@ -256,7 +224,8 @@ SongEditor.prototype.refreshCanvas = function() this.ctx.lineTo(cursorX + 6, cursorY2 + 6); this.ctx.lineTo(cursorX, cursorY2); this.ctx.fill(); - }*/ + } + } // Draw side fade-outs. var leftFadeGradient = this.ctx.createLinearGradient(0, 0, this.MARGIN_LEFT, 0); @@ -292,7 +261,7 @@ SongEditor.prototype.drawNote = function(region, pitch, tick, duration, hovering var pos = this.getNotePosition(region, row, clippedStartTick, clippedDuration); - this.drawDegreeColoredRectangle(deg, pos.x1, pos.y1, pos.x2, pos.y2); + this.drawDegreeColoredRectangle(deg, pos.x1 + this.NOTE_MARGIN_HOR, pos.y1, pos.x2 - this.NOTE_MARGIN_HOR, pos.y2); // Draw highlights. this.ctx.save(); @@ -315,23 +284,25 @@ SongEditor.prototype.drawNote = function(region, pitch, tick, duration, hovering } -SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hovering, selected) +SongEditor.prototype.drawChord = function(region, chord, tick, duration, hovering, selected) { - var block = this.viewBlocks[blockIndex]; - - // Check if the note is inside the block. - if (tick + duration <= block.tick || - tick >= block.tick + block.duration) + // Check if the chord is inside the region. + if (tick + duration <= region.tick || + tick >= region.tick + region.duration) return; - var deg = theory.getDegreeForPitch(chord.rootPitch, block.key.scale, block.key.tonicPitch); - var pos = this.getChordPosition(block, tick, duration); + var clippedStartTick = Math.max(tick, region.tick); + var clippedEndTick = Math.min(tick + duration, region.tick + region.duration); + var clippedDuration = clippedEndTick - clippedStartTick; + + var deg = this.theory.getDegreeForPitch(chord.rootPitch, region.key.scale, region.key.tonicPitch); + var pos = this.getChordPosition(region, clippedStartTick, clippedDuration); - this.drawDegreeColoredRectangle(deg, { x1: pos.x1, y1: pos.y1, x2: pos.x2, y2: pos.y1 + this.CHORD_ORNAMENT_HEIGHT }); - this.drawDegreeColoredRectangle(deg, { x1: pos.x1, y1: pos.y2 - this.CHORD_ORNAMENT_HEIGHT, x2: pos.x2, y2: pos.y2 }); + this.drawDegreeColoredRectangle(deg, pos.x1 + this.NOTE_MARGIN_HOR, pos.y1, pos.x2 - this.NOTE_MARGIN_HOR, pos.y1 + this.CHORD_ORNAMENT_HEIGHT); + this.drawDegreeColoredRectangle(deg, pos.x1 + this.NOTE_MARGIN_HOR, pos.y2 - this.CHORD_ORNAMENT_HEIGHT, pos.x2 - this.NOTE_MARGIN_HOR, pos.y2); // Draw roman symbol. - var numeral = theory.getRomanNumeralForPitch(chord.rootPitch, block.key.scale, block.key.tonicPitch); + var numeral = this.theory.getRomanNumeralForPitch(chord.rootPitch, region.key.scale, region.key.tonicPitch); var romanText = chord.chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); this.ctx.fillStyle = "#000000"; @@ -380,20 +351,98 @@ SongEditor.prototype.drawChord = function(blockIndex, chord, tick, duration, hov } this.ctx.restore(); +} + + +SongEditor.prototype.drawKeyChange = function(region, keyChange, tick, hovering, selected) +{ + // Check if the key change is inside the region. + if (tick < region.tick || + tick >= region.tick + region.duration) + return; + + var COLOR = "#aaaaaa"; + + var x = region.x1 + (tick - region.tick) * this.tickZoom; - // Draw part between blocks. - if (tick + duration > block.tick + block.duration && blockIndex < this.viewBlocks.length - 1) + this.ctx.strokeStyle = COLOR; + this.ctx.beginPath(); + this.ctx.moveTo(x, region.y1); + this.ctx.lineTo(x, region.y2); + this.ctx.stroke(); + + if (selected) + { + this.ctx.save(); + this.ctx.globalAlpha = 0.6; + this.ctx.fillStyle = COLOR; + this.ctx.fillRect(x, region.y1, region.x2 - x, this.HEADER_LINE_HEIGHT); + this.ctx.restore(); + } + else if (hovering) { - var nextBlock = this.viewBlocks[blockIndex + 1]; - var col = theory.getColorForDegree(deg - (deg % 1)); + this.ctx.save(); + this.ctx.globalAlpha = 0.2; + this.ctx.fillStyle = COLOR; + this.ctx.fillRect(x, region.y1, region.x2 - x, this.HEADER_LINE_HEIGHT); + this.ctx.restore(); + } + + this.ctx.font = "14px Tahoma"; + this.ctx.textAlign = "left"; + this.ctx.textBaseline = "middle"; + this.ctx.fillStyle = COLOR; + this.ctx.fillText( + this.theory.getNameForPitch(keyChange.tonicPitch, keyChange.scale, keyChange.tonicPitch) + " " + keyChange.scale.name, + x + 8, + region.y1 + 8, + region.x2 - x - 16); +} + + +SongEditor.prototype.drawMeterChange = function(region, meterChange, tick, hovering, selected) +{ + // Check if the meter change is inside the region. + if (tick < region.tick || + tick >= region.tick + region.duration) + return; + var COLOR = "#88aaaa"; + + var x = region.x1 + (tick - region.tick) * this.tickZoom; + + this.ctx.strokeStyle = COLOR; + this.ctx.beginPath(); + this.ctx.moveTo(x, region.y1 + this.HEADER_LINE_HEIGHT); + this.ctx.lineTo(x, region.y2); + this.ctx.stroke(); + + if (selected) + { this.ctx.save(); - this.ctx.globalAlpha = 0.5; - this.ctx.fillStyle = col; - this.drawDegreeColoredRectangle(deg, { x1: block.x2, y1: pos.y1, x2: nextBlock.x1, y2: pos.y1 + this.CHORD_ORNAMENT_HEIGHT } ); - this.drawDegreeColoredRectangle(deg, { x1: block.x2, y1: pos.y2 - this.CHORD_ORNAMENT_HEIGHT, x2: nextBlock.x1, y2: pos.y2 }); + this.ctx.globalAlpha = 0.6; + this.ctx.fillStyle = COLOR; + this.ctx.fillRect(x, region.y1 + this.HEADER_LINE_HEIGHT, region.x2 - x, this.HEADER_LINE_HEIGHT); this.ctx.restore(); } + else if (hovering) + { + this.ctx.save(); + this.ctx.globalAlpha = 0.2; + this.ctx.fillStyle = COLOR; + this.ctx.fillRect(x, region.y1 + this.HEADER_LINE_HEIGHT, region.x2 - x, this.HEADER_LINE_HEIGHT); + this.ctx.restore(); + } + + this.ctx.font = "14px Tahoma"; + this.ctx.textAlign = "left"; + this.ctx.textBaseline = "middle"; + this.ctx.fillStyle = COLOR; + this.ctx.fillText( + "" + meterChange.numerator + " / " + meterChange.denominator, + x + 8, + region.y1 + 24, + region.x2 - x - 16); } diff --git a/src/editor_event_handling.js b/src/editor_event_handling.js index aaaaa86..993ccd1 100644 --- a/src/editor_event_handling.js +++ b/src/editor_event_handling.js @@ -20,10 +20,10 @@ SongEditor.prototype.handleMouseMove = function(ev) // Check what's under the mouse, if it's not down. if (!this.mouseDown) { - for (var r = 0; r < this.viewRegions.length; r++) + var regionIndex = this.getRegionAtPosition(mousePos); + if (regionIndex != -1) { - var region = this.viewRegions[r]; - + var region = this.viewRegions[regionIndex]; if (isPointInside(mousePos, region)) { // Check for notes. @@ -55,21 +55,23 @@ SongEditor.prototype.handleMouseMove = function(ev) } // Check for chords. - /*if (this.hoverNote < 0) + if (this.hoverNote < 0) { - for (var n = 0; n < this.viewBlocks[b].chords.length; n++) + for (var n = 0; n < region.chords.length; n++) { - var chord = this.viewBlocks[b].chords[n]; - if (isPointInside(mousePos, chord)) + var chord = this.songData.chords[region.chords[n]]; + var chordPos = this.getChordPosition(region, chord.tick, chord.duration); + + if (isPointInside(mousePos, chordPos)) { - this.hoverChord = chord.chordIndex; + this.hoverChord = region.chords[n]; - if (mousePos.x <= chord.resizeHandleL + this.NOTE_STRETCH_MARGIN) + if (mousePos.x <= chordPos.x1 + this.NOTE_STRETCH_MARGIN) { this.canvas.style.cursor = "ew-resize"; this.hoverStretchL = true; } - else if (mousePos.x >= chord.resizeHandleR - this.NOTE_STRETCH_MARGIN) + else if (mousePos.x >= chordPos.x2 - this.NOTE_STRETCH_MARGIN) { this.canvas.style.cursor = "ew-resize"; this.hoverStretchR = true; @@ -80,41 +82,43 @@ SongEditor.prototype.handleMouseMove = function(ev) break; } } - }*/ - - break; + } } - } - - // Check for key changes. - /*if (this.hoverNote < 0 && this.hoverChord < 0) - { - for (var n = 0; n < this.viewKeyChanges.length; n++) + + // Check for key changes. + if (this.hoverNote < 0 && this.hoverChord < 0) { - var keyChange = this.viewKeyChanges[n]; - if (isPointInside(mousePos, keyChange)) + for (var n = 0; n < region.keyChanges.length; n++) { - this.hoverKeyChange = keyChange.keyChangeIndex; - this.canvas.style.cursor = "ew-resize"; - break; - } - } - } - - // Check for meter changes. - if (this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) - { - for (var n = 0; n < this.viewMeterChanges.length; n++) + var keyChange = this.songData.keyChanges[region.keyChanges[n]]; + var keyChangePos = this.getKeyChangePosition(region, keyChange.tick); + + if (isPointInside(mousePos, keyChangePos)) + { + this.hoverKeyChange = region.keyChanges[n]; + this.canvas.style.cursor = "pointer"; + break; + } + } + } + + // Check for meter changes. + if (this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) { - var meterChange = this.viewMeterChanges[n]; - if (isPointInside(mousePos, meterChange)) + for (var n = 0; n < region.meterChanges.length; n++) { - this.hoverMeterChange = meterChange.meterChangeIndex; - this.canvas.style.cursor = "ew-resize"; - break; - } - } - }*/ + var meterChange = this.songData.meterChanges[region.meterChanges[n]]; + var meterChangePos = this.getMeterChangePosition(region, meterChange.tick); + + if (isPointInside(mousePos, meterChangePos)) + { + this.hoverMeterChange = region.meterChanges[n]; + this.canvas.style.cursor = "pointer"; + break; + } + } + } + } this.refreshCanvas(); } @@ -202,7 +206,7 @@ SongEditor.prototype.handleMouseDown = function(ev) this.chordSelections[this.hoverChord] = true; this.cursorTick = this.songData.chords[this.hoverChord].tick; - theory.playChordSample(this.synth, this.songData.chords[this.hoverChord].chord, this.songData.chords[this.hoverChord].rootPitch); + this.theory.playChordSample(this.synth, this.songData.chords[this.hoverChord].chord, this.songData.chords[this.hoverChord].rootPitch); if (this.hoverStretchR) { diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 437d540..650c3f8 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -140,7 +140,7 @@ SongEditor.prototype.getZoneAtPosition = function(pos) return this.CURSOR_ZONE_ALL; var region = this.viewRegions[regionIndex]; - if (pos.y >= region.y1 + this.HEADER_MARGIN && + if (pos.y >= region.y1 + this.HEADER_HEIGHT && pos.y <= region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) return this.CURSOR_ZONE_NOTES; @@ -274,7 +274,7 @@ SongEditor.prototype.getNoteDragged = function(note, dragPosition) if (this.mouseDragAction == "move") { return { - tick: Math.max(0, note.tick + tickOffset), + tick: Math.max(0, Math.min(this.songData.endTick - note.duration, note.tick + tickOffset)), duration: note.duration, pitch: Math.max(this.theory.getMinPitch(), Math.min(this.theory.getMaxPitch(), note.pitch + pitchOffset)) }; @@ -307,13 +307,13 @@ SongEditor.prototype.getNoteDragged = function(note, dragPosition) SongEditor.prototype.getChordDragged = function(chord, dragPosition) { - var dragTick = this.getTickAtPosition(dragPosition.x); - var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin.x); + var dragTick = this.getTickAtPosition(dragPosition); + var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin); if (this.mouseDragAction == "move") { return { - tick: Math.max(0, chord.tick + tickOffset), + tick: Math.max(0, Math.min(this.songData.endTick - chord.duration, chord.tick + tickOffset)), duration: chord.duration }; } @@ -344,10 +344,10 @@ SongEditor.prototype.getChordDragged = function(chord, dragPosition) SongEditor.prototype.getKeyChangeDragged = function(keyChange, dragPosition) { - var tickOffset = this.getTickAtPosition(dragPosition.x) - this.getTickAtPosition(this.mouseDragOrigin.x); + var tickOffset = this.getTickAtPosition(dragPosition) - this.getTickAtPosition(this.mouseDragOrigin); if (this.mouseDragAction == "move") - return { tick: keyChange.tick + tickOffset }; + return { tick: Math.max(0, Math.min(this.songData.endTick, keyChange.tick + tickOffset)) }; // TODO: Should move proportionally, like notes. else if (this.mouseDragAction == "stretch") @@ -357,10 +357,10 @@ SongEditor.prototype.getKeyChangeDragged = function(keyChange, dragPosition) SongEditor.prototype.getMeterChangeDragged = function(meterChange, dragPosition) { - var tickOffset = this.getTickAtPosition(dragPosition.x) - this.getTickAtPosition(this.mouseDragOrigin.x); + var tickOffset = this.getTickAtPosition(dragPosition) - this.getTickAtPosition(this.mouseDragOrigin); if (this.mouseDragAction == "move") - return { tick: meterChange.tick + tickOffset }; + return { tick: Math.max(0, Math.min(this.songData.endTick, meterChange.tick + tickOffset)) }; // TODO: Should move proportionally, like notes. else if (this.mouseDragAction == "stretch") diff --git a/src/editor_representation.js b/src/editor_representation.js index 99f8f57..b54a322 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -27,8 +27,20 @@ SongEditor.prototype.refreshRepresentation = function() // Iterate through song sections. var currentY = this.MARGIN_TOP; - var currentKeyChangeIndex = 0; - var currentMeterChangeIndex = 0; + var currentKeyChangeIndex = -1; + var currentMeterChangeIndex = -1; + + if (this.songData.keyChanges.length > 0 && + this.songData.keyChanges[0].tick == 0) + currentKeyChangeIndex = 0; + + if (this.songData.meterChanges.length > 0 && + this.songData.meterChanges[0].tick == 0) + currentMeterChangeIndex = 0; + + var DEFAULT_KEY = new SongDataKeyChange(-1, this.theory.scales[0], this.theory.C); + var DEFAULT_METER = new SongDataMeterChange(-1, 4, 4); + for (var i = 0; i < this.songData.sectionBreaks.length + 1; i++) { var sectionTickRange = this.songData.getSectionTickRange(i); @@ -63,10 +75,10 @@ SongEditor.prototype.refreshRepresentation = function() } var regionEndTick = Math.min(nextKeyChangeTick, nextMeterChangeTick); - var keyChange = this.songData.keyChanges[currentKeyChangeIndex]; - var meterChange = this.songData.meterChanges[currentMeterChangeIndex]; + var key = (currentKeyChangeIndex >= 0 ? this.songData.keyChanges[currentKeyChangeIndex] : DEFAULT_KEY); + var meter = (currentMeterChangeIndex >= 0 ? this.songData.meterChanges[currentMeterChangeIndex] : DEFAULT_METER); - // Find lowest and highest pitch in the section. + // Find lowest and highest pitch in section. var notes = []; for (var n = 0; n < this.songData.notes.length; n++) { @@ -75,13 +87,34 @@ SongEditor.prototype.refreshRepresentation = function() if (note.tick < sectionTickRange.end && note.tick + note.duration >= sectionTickRange.start) { - var noteRow = this.theory.getRowForPitch(note.pitch, keyChange.scale, keyChange.tonicPitch); + var noteRow = this.theory.getRowForPitch(note.pitch, key.scale, key.tonicPitch); lowestNoteRow = Math.min(lowestNoteRow, Math.floor(noteRow)); highestNoteRow = Math.max(highestNoteRow, Math.ceil(noteRow) + 1); notes.push(n); } } + // Find chords in section. + var chords = []; + for (var n = 0; n < this.songData.chords.length; n++) + { + var chord = this.songData.chords[n]; + + if (chord.tick < sectionTickRange.end && + chord.tick + chord.duration >= sectionTickRange.start) + { + chords.push(n); + } + } + + var keyChanges = []; + if (key.tick == currentRegionStart) + keyChanges.push(currentKeyChangeIndex); + + var meterChanges = []; + if (meter.tick == currentRegionStart) + meterChanges.push(currentMeterChangeIndex); + // Add region to list. var region = { tick: currentRegionStart, @@ -90,11 +123,12 @@ SongEditor.prototype.refreshRepresentation = function() y1: 0, x2: this.MARGIN_LEFT + (regionEndTick - sectionTickRange.start) * this.tickZoom, y2: 0, - showKeyChange: this.songData.keyChanges[currentKeyChangeIndex].tick == currentRegionStart, - showMeterChange: this.songData.meterChanges[currentMeterChangeIndex].tick == currentRegionStart, - key: this.songData.keyChanges[currentKeyChangeIndex], - meter: this.songData.meterChanges[currentMeterChangeIndex], - notes: notes + key: key, + meter: meter, + notes: notes, + chords: chords, + keyChanges: keyChanges, + meterChanges: meterChanges }; regions.push(region); @@ -149,4 +183,46 @@ SongEditor.prototype.getNotePosition = function(region, row, tick, duration) x2: x1 + duration * this.tickZoom, y2: y1 + this.NOTE_HEIGHT }; +} + + +SongEditor.prototype.getChordPosition = function(region, tick, duration) +{ + var x1 = region.x1 + (tick - region.tick) * this.tickZoom; + var y1 = region.y2 - this.CHORD_HEIGHT; + + return { + x1: x1, + y1: y1, + x2: x1 + duration * this.tickZoom, + y2: y1 + this.CHORD_HEIGHT + }; +} + + +SongEditor.prototype.getKeyChangePosition = function(region, tick) +{ + var x1 = region.x1 + (tick - region.tick) * this.tickZoom; + var y1 = region.y1; + + return { + x1: x1, + y1: y1, + x2: region.x2, + y2: y1 + this.HEADER_LINE_HEIGHT + }; +} + + +SongEditor.prototype.getMeterChangePosition = function(region, tick) +{ + var x1 = region.x1 + (tick - region.tick) * this.tickZoom; + var y1 = region.y1 + this.HEADER_LINE_HEIGHT; + + return { + x1: x1, + y1: y1, + x2: region.x2, + y2: y1 + this.HEADER_LINE_HEIGHT + }; } \ No newline at end of file From fcaa702774e77d136cb8ec84fb4f1565d829a485 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Mon, 25 Apr 2016 23:09:57 -0300 Subject: [PATCH 24/46] WIP: add section resizing --- src/editor.js | 2 + src/editor_drawing.js | 198 ++++++++++++++++++++++++++--------- src/editor_event_handling.js | 107 ++++++++++++++----- src/editor_interaction.js | 17 ++- src/editor_representation.js | 43 +++++++- src/songdata.js | 182 ++++++++++++++++++++++++++++++-- 6 files changed, 462 insertions(+), 87 deletions(-) diff --git a/src/editor.js b/src/editor.js index 5769904..423fad9 100644 --- a/src/editor.js +++ b/src/editor.js @@ -55,12 +55,14 @@ function SongEditor(canvas, theory, synth, songData) this.mouseDragOrigin = { x: 0, y: 0 }; this.mouseStretchPivotTick = 0; this.mouseStretchOriginTick = 0; + this.mouseDraggedSectionKnob = -1; // These indicate which object the mouse is currently hovering over. this.hoverNote = -1; this.hoverChord = -1; this.hoverKeyChange = -1; this.hoverMeterChange = -1; + this.hoverSectionKnob = -1; this.hoverStretchR = false; this.hoverStretchL = false; diff --git a/src/editor_drawing.js b/src/editor_drawing.js index e97d360..e1e0886 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -25,61 +25,18 @@ SongEditor.prototype.refreshCanvas = function() { var region = this.viewRegions[r]; - // Draw region boundaries. - this.ctx.strokeStyle = SECTION_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - - this.ctx.strokeRect( - region.x1, - region.y1 + this.HEADER_HEIGHT, - region.x2 - region.x1, - (region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - (region.y1 + this.HEADER_HEIGHT)); - - this.ctx.strokeRect( - region.x1, - region.y2 - this.CHORD_HEIGHT, - region.x2 - region.x1, - this.CHORD_HEIGHT); - - this.ctx.stroke(); - - // Draw note lines. - this.ctx.strokeStyle = NOTE_LINE_COLOR; - this.ctx.beginPath(); - for (var n = 1; n < region.highestNoteRow - region.lowestNoteRow; n++) - { - var y = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - n * this.NOTE_HEIGHT; - this.ctx.moveTo(region.x1 + 1, y); - this.ctx.lineTo(region.x2 - 1, y); - } - this.ctx.stroke(); - - // Draw beat lines. - var beatCount = 0; - for (var n = region.meter.tick - region.tick; n < region.duration; n += this.songData.ticksPerWholeNote / region.meter.denominator) + // Draw section-stretched or regular region. + if (this.mouseDragAction == "stretch-section" && this.mouseDraggedSectionKnob == region.section) { - if (n != 0) - { - this.ctx.strokeStyle = (beatCount == 0 ? MEASURE_COLOR_STRONG : MEASURE_COLOR); - this.ctx.beginPath(); - this.ctx.moveTo( - region.x1 + n * this.tickZoom, - region.y1 + this.HEADER_HEIGHT + 1); - this.ctx.lineTo( - region.x1 + n * this.tickZoom, - region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - 1); - this.ctx.moveTo( - region.x1 + n * this.tickZoom, - region.y2 - this.CHORD_HEIGHT + 1); - this.ctx.lineTo( - region.x1 + n * this.tickZoom, - region.y2 - 1); - this.ctx.stroke(); - } + var knobDraggedTick = this.getSectionKnobDraggedTick(region, this.mouseDragCurrent); - beatCount = (beatCount + 1) % region.meter.numerator; + if (region.sectionKnob && knobDraggedTick > region.tick + region.duration) + this.drawRegion(region, region.tick, knobDraggedTick - region.tick); + else + this.drawRegion(region, region.tick, region.duration, false); } + else + this.drawRegion(region, region.tick, region.duration, false); this.ctx.save(); this.ctx.rect( @@ -189,6 +146,18 @@ SongEditor.prototype.refreshCanvas = function() } } + // Draw section knob and section deletion. + if (this.mouseDragAction == "stretch-section" && this.mouseDraggedSectionKnob == region.section) + { + var knobDraggedTick = this.getSectionKnobDraggedTick(region, this.mouseDragCurrent); + this.drawSectionKnob(region, knobDraggedTick, region.section == this.hoverSectionKnob, true, false); + + if (knobDraggedTick < region.tick + region.duration) + this.drawSectionDeletion(region, knobDraggedTick, region.tick + region.duration - knobDraggedTick); + } + else if (region.sectionKnob) + this.drawSectionKnob(region, region.tick + region.duration, region.section == this.hoverSectionKnob, false, true); + // Draw cursor. if (this.showCursor && this.cursorTick >= region.tick && this.cursorTick <= region.tick + region.duration) { @@ -366,6 +335,8 @@ SongEditor.prototype.drawKeyChange = function(region, keyChange, tick, hovering, var x = region.x1 + (tick - region.tick) * this.tickZoom; this.ctx.strokeStyle = COLOR; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); this.ctx.moveTo(x, region.y1); this.ctx.lineTo(x, region.y2); @@ -412,6 +383,7 @@ SongEditor.prototype.drawMeterChange = function(region, meterChange, tick, hover var x = region.x1 + (tick - region.tick) * this.tickZoom; this.ctx.strokeStyle = COLOR; + this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.moveTo(x, region.y1 + this.HEADER_LINE_HEIGHT); this.ctx.lineTo(x, region.y2); @@ -446,6 +418,128 @@ SongEditor.prototype.drawMeterChange = function(region, meterChange, tick, hover } +SongEditor.prototype.drawSectionKnob = function(region, tick, hovering, selected, clipOutside) +{ + // Check if the knob is inside the region. + if (clipOutside && + (tick < region.tick || + tick > region.tick + region.duration)) + return; + + var COLOR = "#000000"; + var COLOR_HOVER = "#88ddff"; + var COLOR_SELECTED = "#4488ff"; + + var x = region.x1 + (tick - region.tick) * this.tickZoom; + + this.ctx.strokeStyle = (selected ? COLOR_SELECTED : hovering ? COLOR_HOVER : COLOR); + this.ctx.fillStyle = (selected ? COLOR_SELECTED : hovering ? COLOR_HOVER : COLOR); + this.ctx.lineWidth = 2; + + this.ctx.beginPath(); + this.ctx.moveTo(x, region.y1 + this.HEADER_HEIGHT - 12); + this.ctx.lineTo(x, region.y2); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.arc(x, region.y1 + this.HEADER_HEIGHT - 12, 6, 0, Math.PI * 2); + this.ctx.fill(); +} + + +SongEditor.prototype.drawSectionDeletion = function(region, tick, duration) +{ + // Check if the knob is inside the region. + if (tick + duration < region.tick || + tick >= region.tick + region.duration) + return; + + var COLOR = "#ffffff"; + + var clippedStart = Math.max(region.tick, tick); + var clippedEnd = Math.min(region.tick + region.duration, tick + duration); + + var x1 = region.x1 + (clippedStart - region.tick) * this.tickZoom; + var x2 = region.x1 + (clippedEnd - region.tick) * this.tickZoom; + + this.ctx.save(); + this.ctx.fillStyle = COLOR; + this.ctx.globalAlpha = 0.8; + + this.ctx.fillRect(x1 + 1, region.y1 + this.HEADER_HEIGHT - 1, x2 - x1, region.y2 - region.y1 - this.HEADER_HEIGHT + 2); + + this.ctx.restore(); +} + + +SongEditor.prototype.drawRegion = function(region, tick, duration, asSectionStretch) +{ + var SECTION_BORDER_COLOR = "#000000"; + var NOTE_LINE_COLOR = "#dddddd"; + var MEASURE_COLOR = "#dddddd"; + var MEASURE_COLOR_STRONG = "#888888"; + + var x1 = region.x1 + (tick - region.tick) * this.tickZoom; + var x2 = region.x1 + (tick + duration - region.tick) * this.tickZoom; + + // Draw region boundaries. + this.ctx.strokeStyle = SECTION_BORDER_COLOR; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + + this.ctx.strokeRect( + x1, + region.y1 + this.HEADER_HEIGHT, + x2 - x1, + (region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - (region.y1 + this.HEADER_HEIGHT)); + + this.ctx.strokeRect( + x1, + region.y2 - this.CHORD_HEIGHT, + x2 - x1, + this.CHORD_HEIGHT); + + this.ctx.stroke(); + + // Draw note lines. + this.ctx.strokeStyle = NOTE_LINE_COLOR; + this.ctx.beginPath(); + for (var n = 1; n < region.highestNoteRow - region.lowestNoteRow; n++) + { + var y = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - n * this.NOTE_HEIGHT; + this.ctx.moveTo(x1 + 1, y); + this.ctx.lineTo(x2 - 1, y); + } + this.ctx.stroke(); + + // Draw beat lines. + var beatCount = 0; + for (var n = region.meter.tick - tick; n < duration; n += this.songData.ticksPerWholeNote / region.meter.denominator) + { + if (n > 0) + { + this.ctx.strokeStyle = (beatCount == 0 ? MEASURE_COLOR_STRONG : MEASURE_COLOR); + this.ctx.beginPath(); + this.ctx.moveTo( + x1 + n * this.tickZoom, + region.y1 + this.HEADER_HEIGHT + 1); + this.ctx.lineTo( + x1 + n * this.tickZoom, + region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - 1); + this.ctx.moveTo( + x1 + n * this.tickZoom, + region.y2 - this.CHORD_HEIGHT + 1); + this.ctx.lineTo( + x1 + n * this.tickZoom, + region.y2 - 1); + this.ctx.stroke(); + } + + beatCount = (beatCount + 1) % region.meter.numerator; + } +} + + SongEditor.prototype.drawDegreeColoredRectangle = function(deg, x1, y1, x2, y2) { var col = this.theory.getColorForDegree(deg - (deg % 1)); diff --git a/src/editor_event_handling.js b/src/editor_event_handling.js index 993ccd1..bbc20ab 100644 --- a/src/editor_event_handling.js +++ b/src/editor_event_handling.js @@ -24,45 +24,59 @@ SongEditor.prototype.handleMouseMove = function(ev) if (regionIndex != -1) { var region = this.viewRegions[regionIndex]; - if (isPointInside(mousePos, region)) + if (isPointInside(mousePos, region.interactBBox)) { + // Check for the section knob. + if (region.sectionKnob) + { + var knobPos = this.getSectionKnobPosition(region); + if (isPointInside(mousePos, knobPos)) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverSectionKnob = region.section; + } + } + // Check for notes. - for (var n = 0; n < region.notes.length; n++) + if (this.hoverSectionKnob < 0) { - var note = this.songData.notes[region.notes[n]]; - var noteRow = this.theory.getRowForPitch(note.pitch, region.key.scale, region.key.tonicPitch); - var notePos = this.getNotePosition(region, noteRow, note.tick, note.duration); - - if (isPointInside(mousePos, notePos)) + for (var n = 0; n < region.notes.length; n++) { - this.hoverNote = region.notes[n]; + var note = this.songData.notes[region.notes[n]]; + var noteRow = this.theory.getRowForPitch(note.pitch, region.key.scale, region.key.tonicPitch); + var notePos = this.getNotePosition(region, noteRow, note.tick, note.duration); - if (mousePos.x <= notePos.x1 + this.NOTE_STRETCH_MARGIN) + if (isPointInside(mousePos, notePos) && isPointInside(mousePos, region)) { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchL = true; - } - else if (mousePos.x >= notePos.x2 - this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchR = true; - } - else - this.canvas.style.cursor = "pointer"; + this.hoverNote = region.notes[n]; + + if (mousePos.x <= notePos.x1 + this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchL = true; + } + else if (mousePos.x >= notePos.x2 - this.NOTE_STRETCH_MARGIN) + { + this.canvas.style.cursor = "ew-resize"; + this.hoverStretchR = true; + } + else + this.canvas.style.cursor = "pointer"; - break; + break; + } } } // Check for chords. - if (this.hoverNote < 0) + if (this.hoverSectionKnob < 0 && this.hoverNote < 0) { for (var n = 0; n < region.chords.length; n++) { var chord = this.songData.chords[region.chords[n]]; var chordPos = this.getChordPosition(region, chord.tick, chord.duration); - if (isPointInside(mousePos, chordPos)) + if (isPointInside(mousePos, chordPos) && isPointInside(mousePos, region)) { this.hoverChord = region.chords[n]; @@ -86,7 +100,7 @@ SongEditor.prototype.handleMouseMove = function(ev) } // Check for key changes. - if (this.hoverNote < 0 && this.hoverChord < 0) + if (this.hoverSectionKnob < 0 && this.hoverNote < 0 && this.hoverChord < 0) { for (var n = 0; n < region.keyChanges.length; n++) { @@ -103,7 +117,7 @@ SongEditor.prototype.handleMouseMove = function(ev) } // Check for meter changes. - if (this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) + if (this.hoverSectionKnob < 0 && this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) { for (var n = 0; n < region.meterChanges.length; n++) { @@ -135,6 +149,12 @@ SongEditor.prototype.handleMouseMove = function(ev) this.refreshCanvas(); } + else if (this.mouseDragAction == "stretch-section") + { + this.canvas.style.cursor = "ew-resize"; + this.refreshCanvas(); + } + else if (this.mouseDragAction == "scroll") { /*var rowOffset = (mousePos.y - this.mouseDragOrigin.y) / this.NOTE_HEIGHT; @@ -173,7 +193,14 @@ SongEditor.prototype.handleMouseDown = function(ev) this.showCursor = true; // Start a dragging operation. - if (this.hoverNote >= 0) + if (this.hoverSectionKnob >= 0) + { + this.unselectAll(); + this.showCursor = false; + this.mouseDragAction = "stretch-section"; + this.mouseDraggedSectionKnob = this.hoverSectionKnob; + } + else if (this.hoverNote >= 0) { this.showCursor = false; if (!this.noteSelections[this.hoverNote]) @@ -262,8 +289,36 @@ SongEditor.prototype.handleMouseUp = function(ev) if (!this.interactionEnabled) return; + // Apply dragged section knob. + if (this.mouseDown && this.mouseDragAction == "stretch-section") + { + var regionStretched = null; + for (var r = 0; r < this.viewRegions.length; r++) + { + var region = this.viewRegions[r]; + if (region.section == this.mouseDraggedSectionKnob && region.sectionKnob) + { + regionStretched = region; + break; + } + } + + var draggedKnobTick = this.getSectionKnobDraggedTick(regionStretched, mousePos); + + if (draggedKnobTick > region.tick + region.duration) + { + this.songData.insertWhitespace(region.tick + region.duration, draggedKnobTick - region.tick - region.duration); + this.refreshRepresentation(); + } + else if (draggedKnobTick < region.tick + region.duration) + { + this.songData.remove(draggedKnobTick, region.tick + region.duration - draggedKnobTick); + this.refreshRepresentation(); + } + } + // Apply dragged modifications. - if (this.mouseDown && this.mouseDragAction != null && this.mouseDragAction != "scroll") + else if (this.mouseDown && this.mouseDragAction != null && this.mouseDragAction != "scroll") { // Store modified objects in a local array and // remove them from the song data. diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 650c3f8..2a39fc2 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -81,6 +81,7 @@ SongEditor.prototype.clearHover = function() this.hoverMeterChange = -1; this.hoverStretchR = false; this.hoverStretchL = false; + this.hoverSectionKnob = -1; } @@ -114,7 +115,7 @@ SongEditor.prototype.getRegionAtPosition = function(pos) { var region = this.viewRegions[r]; - if (isPointInside(pos, region)) + if (isPointInside(pos, region.interactBBox)) return r; } @@ -133,6 +134,12 @@ SongEditor.prototype.getTickAtPosition = function(pos) } +SongEditor.prototype.getTickAtPositionAtRegion = function(region, pos) +{ + return Math.round((region.tick + Math.ceil((pos.x - region.x1) / this.tickZoom)) / this.tickSnap) * this.tickSnap; +} + + SongEditor.prototype.getZoneAtPosition = function(pos) { var regionIndex = this.getRegionAtPosition(pos); @@ -365,4 +372,12 @@ SongEditor.prototype.getMeterChangeDragged = function(meterChange, dragPosition) // TODO: Should move proportionally, like notes. else if (this.mouseDragAction == "stretch") return { tick: meterChange.tick }; +} + + +SongEditor.prototype.getSectionKnobDraggedTick = function(region, dragPosition) +{ + var tickOffset = this.getTickAtPositionAtRegion(region, dragPosition) - this.getTickAtPosition(this.mouseDragOrigin); + + return Math.max(region.sectionTick, region.sectionTick + region.sectionDuration + tickOffset); } \ No newline at end of file diff --git a/src/editor_representation.js b/src/editor_representation.js index b54a322..67004e3 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -117,23 +117,36 @@ SongEditor.prototype.refreshRepresentation = function() // Add region to list. var region = { + section: i, + sectionTick: sectionTickRange.start, + sectionDuration: sectionDuration, tick: currentRegionStart, duration: regionEndTick - currentRegionStart, x1: this.MARGIN_LEFT + (currentRegionStart - sectionTickRange.start) * this.tickZoom, y1: 0, x2: this.MARGIN_LEFT + (regionEndTick - sectionTickRange.start) * this.tickZoom, y2: 0, + + interactBBox: { + x1: this.MARGIN_LEFT + (currentRegionStart - sectionTickRange.start) * this.tickZoom, + y1: 0, + x2: this.MARGIN_LEFT + (regionEndTick - sectionTickRange.start) * this.tickZoom, + y2: 0, + }, + key: key, meter: meter, notes: notes, chords: chords, keyChanges: keyChanges, - meterChanges: meterChanges + meterChanges: meterChanges, + sectionKnob: false }; regions.push(region); this.viewRegions.push(region); + // Prepare for the next region. currentRegionStart = regionEndTick; if (nextKeyChangeTick == nextMeterChangeTick && willChangeKey && willChangeMeter) @@ -151,7 +164,7 @@ SongEditor.prototype.refreshRepresentation = function() } } - // Add the section representation. + // Prepare for the next section. var sectionHeight = this.HEADER_HEIGHT + (highestNoteRow - lowestNoteRow) * this.NOTE_HEIGHT + @@ -163,6 +176,18 @@ SongEditor.prototype.refreshRepresentation = function() regions[r].highestNoteRow = highestNoteRow; regions[r].y1 = currentY; regions[r].y2 = currentY + sectionHeight; + + regions[r].interactBBox.y1 = currentY; + regions[r].interactBBox.y2 = currentY + sectionHeight + this.SECTION_SEPARATION; + + if (r == 0) + regions[r].interactBBox.x1 = 0; + + if (r == regions.length - 1) + { + regions[r].sectionKnob = true; + regions[r].interactBBox.x2 = this.canvasWidth; + } } currentY += sectionHeight + this.SECTION_SEPARATION; @@ -225,4 +250,18 @@ SongEditor.prototype.getMeterChangePosition = function(region, tick) x2: region.x2, y2: y1 + this.HEADER_LINE_HEIGHT }; +} + + +SongEditor.prototype.getSectionKnobPosition = function(region) +{ + var x1 = region.x1 + (region.duration) * this.tickZoom - 12; + var y1 = region.y1; + + return { + x1: x1, + y1: y1, + x2: x1 + 24, + y2: y1 + this.HEADER_HEIGHT + }; } \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js index 1393ae3..d78200f 100644 --- a/src/songdata.js +++ b/src/songdata.js @@ -10,8 +10,6 @@ function SongDataNote(tick, duration, pitch) this.tick = tick; this.duration = duration; this.pitch = pitch; - this.startSection = -1; - this.endSection = -1; } @@ -21,8 +19,6 @@ function SongDataChord(tick, duration, chord, rootPitch) this.duration = duration; this.chord = chord; this.rootPitch = rootPitch; - this.startSection = -1; - this.endSection = -1; } @@ -31,7 +27,6 @@ function SongDataKeyChange(tick, scale, tonicPitch) this.tick = tick; this.scale = scale; this.tonicPitch = tonicPitch; - this.section = -1; } @@ -40,7 +35,6 @@ function SongDataMeterChange(tick, numerator, denominator) this.tick = tick; this.numerator = numerator; this.denominator = denominator; - this.section = -1; } @@ -248,6 +242,182 @@ SongData.prototype.removeChordsByTickRange = function(tickBegin, tickEnd) } +// Remove key changes that fall in the range ]tickBegin, tickEnd[. +SongData.prototype.removeKeyChangesByTickRange = function(tickBegin, tickEnd) +{ + for (var i = this.keyChanges.length - 1; i >= 0; i--) + { + var keyChange = this.keyChanges[i]; + + if (keyChange.tick > tickBegin && keyChange.tick < tickEnd) + this.keyChanges.splice(i, 1); + } +} + + +// Remove meter changes that fall in the range ]tickBegin, tickEnd[. +SongData.prototype.removeMeterChangesByTickRange = function(tickBegin, tickEnd) +{ + for (var i = this.meterChanges.length - 1; i >= 0; i--) + { + var meterChange = this.meterChanges[i]; + + if (meterChange.tick > tickBegin && meterChange.tick < tickEnd) + this.meterChanges.splice(i, 1); + } +} + + +// Split the given note in two, at the given tick. +SongData.prototype.splitNote = function(noteIndex, tick) +{ + var note = this.notes[noteIndex]; + + if (tick <= note.tick || tick >= note.tick + note.duration) + return; + + var noteClone = new SongDataNote(note.tick, note.duration, note.pitch); + + noteClone.tick = tick; + noteClone.duration = note.tick + note.duration - tick; + note.duration = tick - note.tick; + + this.notes.splice(noteIndex + 1, 0, noteClone); +} + + +// Split the given chord in two, at the given tick. +SongData.prototype.splitChord = function(chordIndex, tick) +{ + var chord = this.chords[chordIndex]; + + if (tick <= chord.tick || tick >= chord.tick + chord.duration) + return; + + var chordClone = new SongDataChord(chord.tick, chord.duration, chord.chord, chord.rootPitch); + + chordClone.tick = tick; + chordClone.duration = chord.tick + chord.duration - tick; + chord.duration = tick - chord.tick; + + this.chords.splice(chordIndex + 1, 0, chordClone); +} + + +// Inserts whitespace at the given tick, for the given duration, +// pushing forward any following elements, and returns whether successful. +SongData.prototype.insertWhitespace = function(tick, duration) +{ + if (!this.isValidTick(tick) || !this.isValidTick(tick + duration)) + return false; + + // Split any notes at the insertion tick. + for (var i = this.notes.length - 1; i >= 0; i--) + this.splitNote(i, tick); + + // Split any chords at the insertion tick. + for (var i = this.chords.length - 1; i >= 0; i--) + this.splitChord(i, tick); + + // Displace end tick. + this.endTick += duration; + + // Displace following notes. + for (var i = this.notes.length - 1; i >= 0; i--) + { + if (this.notes[i].tick >= tick) + this.notes[i].tick += duration; + } + + // Displace following chords. + for (var i = this.chords.length - 1; i >= 0; i--) + { + if (this.chords[i].tick >= tick) + this.chords[i].tick += duration; + } + + // Displace following key changes. + for (var i = this.keyChanges.length - 1; i >= 0; i--) + { + if (this.keyChanges[i].tick >= tick) + this.keyChanges[i].tick += duration; + } + + // Displace following meter changes. + for (var i = this.meterChanges.length - 1; i >= 0; i--) + { + if (this.meterChanges[i].tick >= tick) + this.meterChanges[i].tick += duration; + } + + // Displace following section breaks. + for (var i = this.sectionBreaks.length - 1; i >= 0; i--) + { + if (this.sectionBreaks[i].tick >= tick) + this.sectionBreaks[i].tick += duration; + } + + return true; +} + + +// Removes the region starting at the given tick, lasting the given duration, +// removing any contained elements, and returns whether successful. +SongData.prototype.remove = function(tick, duration) +{ + if (!this.isValidTick(tick) || !this.isValidTick(tick + duration)) + return false; + + // Remove any elements at the region. + this.removeNotesByTickRange(tick, tick + duration, null); + this.removeChordsByTickRange(tick, tick + duration); + this.removeKeyChangesByTickRange(tick, tick + duration); + this.removeMeterChangesByTickRange(tick, tick + duration); + + // TODO: Must sanitize elements after displacement. + + // Displace end tick. + this.endTick -= duration; + + // Displace following notes. + for (var i = this.notes.length - 1; i >= 0; i--) + { + if (this.notes[i].tick > tick) + this.notes[i].tick -= duration; + } + + // Displace following chords. + for (var i = this.chords.length - 1; i >= 0; i--) + { + if (this.chords[i].tick > tick) + this.chords[i].tick -= duration; + } + + // Displace following key changes. + for (var i = this.keyChanges.length - 1; i >= 0; i--) + { + if (this.keyChanges[i].tick > tick) + this.keyChanges[i].tick -= duration; + } + + // Displace following meter changes. + for (var i = this.meterChanges.length - 1; i >= 0; i--) + { + if (this.meterChanges[i].tick > tick) + this.meterChanges[i].tick -= duration; + } + + // Displace following section breaks. + for (var i = this.sectionBreaks.length - 1; i >= 0; i--) + { + if (this.sectionBreaks[i].tick > tick) + this.sectionBreaks[i].tick -= duration; + } + + return true; +} + + // Gets the start and end ticks for the given section index. SongData.prototype.getSectionTickRange = function(sectionIndex) { From 27227d7a73e48e3ec405986160d008dcb780b87d Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Mon, 25 Apr 2016 23:58:06 -0300 Subject: [PATCH 25/46] WIP: fix some more editor/toolbox features --- index.html | 6 ++-- main.js | 2 +- src/editor_interaction.js | 22 ++++++------- src/theory.js | 33 ++++++++++--------- src/toolbox.js | 67 ++++++++++++++++++++------------------- 5 files changed, 66 insertions(+), 64 deletions(-) diff --git a/index.html b/index.html index 1ac5f1c..6b25a2c 100644 --- a/index.html +++ b/index.html @@ -8,9 +8,11 @@ - +

-
+
+ +
diff --git a/main.js b/main.js index c8737f5..9abf1e1 100644 --- a/main.js +++ b/main.js @@ -73,5 +73,5 @@ function testOnLoad() var editor = new SongEditor(canvas, theory, synth, song); editor.refreshCanvas(); mainEditor = editor; - //var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor, synth); + var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor, theory, synth); } \ No newline at end of file diff --git a/src/editor_interaction.js b/src/editor_interaction.js index 2a39fc2..98b5380 100644 --- a/src/editor_interaction.js +++ b/src/editor_interaction.js @@ -160,14 +160,14 @@ SongEditor.prototype.getZoneAtPosition = function(pos) } -SongEditor.prototype.getBlockIndexAtTick = function(tick) +SongEditor.prototype.getRegionAtTick = function(tick) { - for (var b = 0; b < this.viewBlocks.length; b++) + for (var r = 0; r < this.viewRegions.length; r++) { - var block = this.viewBlocks[b]; + var region = this.viewRegions[r]; - if (tick >= block.tick && tick < block.tick + block.duration) - return b; + if (tick >= region.tick && tick < region.tick + region.duration) + return r; } return -1; @@ -176,9 +176,9 @@ SongEditor.prototype.getBlockIndexAtTick = function(tick) SongEditor.prototype.getKeyAtTick = function(tick) { - var blockIndex = this.getBlockIndexAtTick(tick); - if (blockIndex >= 0) - return this.viewBlocks[blockIndex].key; + var index = this.getRegionAtTick(tick); + if (index >= 0) + return this.viewRegions[index].key; return null; } @@ -186,9 +186,9 @@ SongEditor.prototype.getKeyAtTick = function(tick) SongEditor.prototype.getMeterAtTick = function(tick) { - var blockIndex = this.getBlockIndexAtTick(tick); - if (blockIndex >= 0) - return this.viewBlocks[blockIndex].meter; + var index = this.getRegionAtTick(tick); + if (index >= 0) + return this.viewRegions[index].meter; return null; } diff --git a/src/theory.js b/src/theory.js index e91ec6f..4c71a11 100644 --- a/src/theory.js +++ b/src/theory.js @@ -34,22 +34,22 @@ function Theory() this.scales = [ { name: "Major", pitches: [ C, D, E, F, G, A, B ] }, - { name: "Dorian" }, - { name: "Phrygian" }, - { name: "Lydian" }, - { name: "Mixolydian" }, - { name: "Natural Minor" }, - { name: "Locrian" }, + { name: "Dorian", pitches: [] }, + { name: "Phrygian", pitches: [] }, + { name: "Lydian", pitches: [] }, + { name: "Mixolydian", pitches: [] }, + { name: "Natural Minor", pitches: [] }, + { name: "Locrian", pitches: [] }, { name: "Harmonic Minor", pitches: [ C, D, Ds, F, G, Gs, B ] }, { name: "Double Harmonic", pitches: [ C, Cs, E, F, G, Gs, B ] }, - { name: "Lydian ♯2 ♯6" }, - { name: "Ultraphrygian" }, - { name: "Hungarian Minor" }, - { name: "Oriental" }, - { name: "Ionian Augmented ♯2" }, - { name: "Locrian ♭♭3 ♭♭7" }, + { name: "Lydian ♯2 ♯6", pitches: [] }, + { name: "Ultraphrygian", pitches: [] }, + { name: "Hungarian Minor", pitches: [] }, + { name: "Oriental", pitches: [] }, + { name: "Ionian Augmented ♯2", pitches: [] }, + { name: "Locrian ♭♭3 ♭♭7", pitches: [] }, { name: "Phrygian Dominant", pitches: [ C, Cs, E, F, G, Gs, As ] }, ]; @@ -57,9 +57,8 @@ function Theory() // Generate the other modes of the non-null scales. for (var i = 0; i < this.scales.length; i++) { - if (!this.scales[i].hasOwnProperty("pitches")) + if (this.scales[i].pitches.length == 0) { - this.scales[i].pitches = []; var offset = this.scales[i - 1].pitches[1]; for (var j = 0; j < 7; j++) { @@ -278,10 +277,10 @@ function Theory() // Plays a sample of the given scale. this.playScaleSample = function(synth, scale, tonicPitch) { - for (var k = 0; k < scale.degrees.length; k++) + for (var k = 0; k < scale.pitches.length; k++) { - synth.playNoteDelayed((tonicPitch + scale.degrees[k]) + 60, k * 60, 960 / 8, 1); + synth.playNoteDelayed((tonicPitch + scale.pitches[k]) + 60, k * 60, 960 / 8, 1); } - synth.playNoteDelayed((tonicPitch + scale.degrees[0] + 12) + 60, scale.degrees.length * 60, 960 / 8, 1); + synth.playNoteDelayed((tonicPitch + scale.pitches[0] + 12) + 60, scale.pitches.length * 60, 960 / 8, 1); } } diff --git a/src/toolbox.js b/src/toolbox.js index 2e33ac0..d593db1 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -1,5 +1,9 @@ -function Toolbox(div, editor, synth) +function Toolbox(div, editor, theory, synth) { + this.editor = editor; + this.theory = theory; + this.synth = synth; + this.div = div; this.playing = false; this.playTimer = null; @@ -101,7 +105,7 @@ function Toolbox(div, editor, synth) this.keyMeterLayout.cells[0][0].appendChild(document.createElement("br")); this.buttonAddKeyChange = document.createElement("button"); - this.buttonAddKeyChange.innerHTML = "Add Key Change"; + this.buttonAddKeyChange.innerHTML = "Insert Key Change"; this.buttonAddKeyChange.className = "toolboxButton"; this.keyMeterLayout.cells[0][0].appendChild(this.buttonAddKeyChange); @@ -145,7 +149,7 @@ function Toolbox(div, editor, synth) this.keyMeterLayout.cells[0][1].appendChild(document.createElement("br")); this.buttonAddMeterChange = document.createElement("button"); - this.buttonAddMeterChange.innerHTML = "Add Meter Change"; + this.buttonAddMeterChange.innerHTML = "Insert Meter Change"; this.buttonAddMeterChange.className = "toolboxButton"; this.keyMeterLayout.cells[0][1].appendChild(this.buttonAddMeterChange); @@ -209,9 +213,6 @@ function Toolbox(div, editor, synth) // Set up callbacks. - this.editor = editor; - this.synth = synth; - var that = this; editor.addOnCursorChanged(function() { that.refresh(); }); editor.addOnSelectionChanged(function() { that.refresh(); }); @@ -224,7 +225,7 @@ function Toolbox(div, editor, synth) this.buttonKeyChangeListen.onclick = function() { var keyChange = that.editor.getKeyAtTick(that.editor.cursorTick); - theory.playScaleSample(that.synth, keyChange.scale, keyChange.tonicPitch); + that.theory.playScaleSample(that.synth, keyChange.scale, keyChange.tonicPitch); } this.buttonAddKeyChange.onclick = function(ev) @@ -430,9 +431,9 @@ Toolbox.prototype.refresh = function() if (keyChange != null) { var scaleIndex = 0; - for (var i = 0; i < theory.scales.length; i++) + for (var i = 0; i < this.theory.scales.length; i++) { - if (keyChange.scale == theory.scales[i]) + if (keyChange.scale == this.theory.scales[i]) { scaleIndex = i; break; @@ -444,7 +445,7 @@ Toolbox.prototype.refresh = function() for (var i = 0; i < 12; i++) { var pitch = i; - this.keyChangeTonicOptions[i].innerHTML = theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); + this.keyChangeTonicOptions[i].innerHTML = this.theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); } this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; @@ -468,18 +469,18 @@ Toolbox.prototype.refresh = function() // Refresh note tools. if (keyChange == null) - keyChange = new SongDataKeyChange(0, theory.scales[0], theory.C); + keyChange = new SongDataKeyChange(0, this.theory.scales[0], this.theory.C); for (var i = 0; i < 12; i++) { var pitch = (i + keyChange.tonicPitch) % 12; - this.buttonNotes[i].innerHTML = theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); + this.buttonNotes[i].innerHTML = this.theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); - var degree = theory.getDegreeForPitch(pitch, keyChange.scale, keyChange.tonicPitch); + var degree = this.theory.getDegreeForPitch(pitch, keyChange.scale, keyChange.tonicPitch); if ((degree % 1) == 0) { - var color = theory.getColorForDegree(degree); + var color = this.theory.getColorForDegree(degree); this.buttonNotes[i].style.background = color; this.buttonNotes[i].style.color = "#000000"; } @@ -499,10 +500,10 @@ Toolbox.prototype.refresh = function() return; ev.preventDefault(); - theory.playNoteSample(that.synth, thisPitch); - var note = new SongDataNote(that.editor.cursorTick, that.editor.WHOLE_NOTE_DURATION / 4, thisPitch); + that.theory.playNoteSample(that.synth, thisPitch); + var note = new SongDataNote(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 4, thisPitch); that.editor.songData.addNote(note); - that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 4; + that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 4; that.editor.showCursor = true; that.editor.cursorZone = that.editor.CURSOR_ZONE_NOTES; that.editor.unselectAll(); @@ -532,9 +533,9 @@ Toolbox.prototype.refresh = function() if ((i + 4) >= 7) pitch3 += 12; - var color = theory.getColorForDegree(i); - var chord = theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); - var numeral = theory.getRomanNumeralForPitch(pitch1, keyChange.scale, keyChange.tonicPitch); + var color = this.theory.getColorForDegree(i); + var chord = this.theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); + var numeral = this.theory.getRomanNumeralForPitch(pitch1, keyChange.scale, keyChange.tonicPitch); var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); romanText += "" + chord.romanSup + ""; @@ -555,10 +556,10 @@ Toolbox.prototype.refresh = function() return; ev.preventDefault(); - theory.playChordSample(that.synth, thisChord, thisPitch); - var chord = new SongDataChord(that.editor.cursorTick, that.editor.WHOLE_NOTE_DURATION / 2, thisChord, thisPitch); + that.theory.playChordSample(that.synth, thisChord, thisPitch); + var chord = new SongDataChord(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 2, thisChord, thisPitch); that.editor.songData.addChord(chord); - that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 2; + that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 2; that.editor.showCursor = true; that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; that.editor.unselectAll(); @@ -582,15 +583,15 @@ Toolbox.prototype.refresh = function() for (var i = 0; i < 12; i++) { var rootPitch = (keyChange.tonicPitch + i) % 12; - var degree = theory.getDegreeForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); + var degree = this.theory.getDegreeForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); var index = inKeyIndex; if ((degree % 1) != 0) index = outOfKeyIndex; - var color = theory.getColorForDegree(degree); - var chord = theory.chords[chordCategory - 1]; - var numeral = theory.getRomanNumeralForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); + var color = this.theory.getColorForDegree(degree); + var chord = this.theory.chords[chordCategory - 1]; + var numeral = this.theory.getRomanNumeralForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); romanText += "" + chord.romanSup + ""; @@ -620,10 +621,10 @@ Toolbox.prototype.refresh = function() return; ev.preventDefault(); - theory.playChordSample(that.synth, thisChord, thisPitch); - var chord = new SongDataChord(that.editor.cursorTick, that.editor.WHOLE_NOTE_DURATION / 2, thisChord, thisPitch); + that.theory.playChordSample(that.synth, thisChord, thisPitch); + var chord = new SongDataChord(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 2, thisChord, thisPitch); that.editor.songData.addChord(chord); - that.editor.cursorTick += that.editor.WHOLE_NOTE_DURATION / 2; + that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 2; that.editor.showCursor = true; that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; that.editor.unselectAll(); @@ -651,7 +652,7 @@ Toolbox.prototype.editKeyChange = function() if (keyChange == null) return; - keyChange.scale = theory.scales[this.keyChangeScaleSelect.selectedIndex]; + keyChange.scale = this.theory.scales[this.keyChangeScaleSelect.selectedIndex]; keyChange.tonicPitch = this.keyChangeTonicSelect.selectedIndex; this.editor.refreshRepresentation(); @@ -683,7 +684,7 @@ Toolbox.prototype.editMeterChange = function() Toolbox.prototype.processPlayback = function() { var bpm = this.editor.songData.beatsPerMinute; - var deltaTicks = bpm / 60 / 60 * (960 / 4); + var deltaTicks = bpm / 60 / 60 * (this.editor.songData.ticksPerWholeNote / 4); var lastCursorTick = this.editor.cursorTick; this.editor.cursorTick += deltaTicks; @@ -704,7 +705,7 @@ Toolbox.prototype.processPlayback = function() if (chord.tick + chord.duration > lastCursorTick && chord.tick < this.editor.cursorTick && !(chord.tick + chord.duration > lastCursorTick && chord.tick + chord.duration < this.editor.cursorTick)) { - var halfTick = this.editor.WHOLE_NOTE_DURATION / 2; + var halfTick = this.editor.songData.ticksPerWholeNote / 2; var quarterTick = halfTick / 2; var eighthTick = quarterTick / 2; From 0108a6ca964bf5ab2e391b991be4624260cf28d9 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Tue, 26 Apr 2016 22:54:06 -0300 Subject: [PATCH 26/46] WIP: restructure toolbox --- index.html | 82 ++++- src/editor_drawing.js | 14 - src/editor_representation.js | 4 + src/toolbox.js | 588 ++++++++++++++--------------------- toolbox.css | 111 +++++++ 5 files changed, 421 insertions(+), 378 deletions(-) create mode 100644 toolbox.css diff --git a/index.html b/index.html index 6b25a2c..f170ef5 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,85 @@ Music Analysis + - -
-
-
- + +
+ +
+
+ +
+ +
+ +
+ Tempo + + +
+ +
+ Key +
+ + + +
+ +
+ +
+ Meter +
+ + / + +
+ +
+ +
+ +
+ +
+
+

+
+ +
+
+ +
+ +
+
diff --git a/src/editor_drawing.js b/src/editor_drawing.js index e1e0886..ffdcbcd 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -196,20 +196,6 @@ SongEditor.prototype.refreshCanvas = function() } } - // Draw side fade-outs. - var leftFadeGradient = this.ctx.createLinearGradient(0, 0, this.MARGIN_LEFT, 0); - leftFadeGradient.addColorStop(0, "rgba(255, 255, 255, 1)"); - leftFadeGradient.addColorStop(1, "rgba(255, 255, 255, 0)"); - this.ctx.fillStyle = leftFadeGradient; - this.ctx.fillRect(0, 0, this.MARGIN_LEFT, this.canvasHeight); - - var rightFadeGradient = this.ctx.createLinearGradient(this.canvasWidth - this.MARGIN_RIGHT, 0, this.canvasWidth, 0); - rightFadeGradient.addColorStop(0, "rgba(255, 255, 255, 0)"); - rightFadeGradient.addColorStop(1, "rgba(255, 255, 255, 1)"); - this.ctx.fillStyle = rightFadeGradient; - this.ctx.fillRect(this.canvasWidth - this.MARGIN_RIGHT, 0, this.MARGIN_RIGHT, this.canvasHeight); - - this.ctx.restore(); } diff --git a/src/editor_representation.js b/src/editor_representation.js index 67004e3..26ff607 100644 --- a/src/editor_representation.js +++ b/src/editor_representation.js @@ -26,6 +26,7 @@ SongEditor.prototype.refreshRepresentation = function() this.meterChangeSelections.push(false); // Iterate through song sections. + var maxX = this.MARGIN_LEFT; var currentY = this.MARGIN_TOP; var currentKeyChangeIndex = -1; var currentMeterChangeIndex = -1; @@ -143,6 +144,8 @@ SongEditor.prototype.refreshRepresentation = function() sectionKnob: false }; + maxX = Math.max(maxX, region.x2 + this.MARGIN_RIGHT); + regions.push(region); this.viewRegions.push(region); @@ -193,6 +196,7 @@ SongEditor.prototype.refreshRepresentation = function() currentY += sectionHeight + this.SECTION_SEPARATION; } + this.canvas.width = maxX + 400; this.canvas.height = currentY; } diff --git a/src/toolbox.js b/src/toolbox.js index d593db1..e93a23c 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -9,214 +9,18 @@ function Toolbox(div, editor, theory, synth) this.playTimer = null; this.playTimerRefresh = null; + this.currentChordKind = 0; - this.initCSS(); - - - // Main table layout. Working with divs is so terrible... - this.mainLayout = this.createTable(this.div, 2, 1); - - // Playback controls and global song settings. - this.buttonPlay = document.createElement("button"); - this.buttonPlay.innerHTML = "▶ Play"; - this.buttonPlay.className = "toolboxButton"; - this.buttonPlay.style.fontSize = "20px"; - this.mainLayout.cells[0][0].appendChild(this.buttonPlay); - - this.buttonRewind = document.createElement("button"); - this.buttonRewind.innerHTML = "◀◀ Rewind"; - this.buttonRewind.className = "toolboxButton"; - this.mainLayout.cells[0][0].appendChild(this.buttonRewind); - - this.mainLayout.cells[0][0].appendChild(document.createElement("br")); - - var inputBPMSpan = document.createElement("span"); - inputBPMSpan.innerHTML = "Tempo "; - inputBPMSpan.className = "toolboxText"; - this.mainLayout.cells[0][0].appendChild(inputBPMSpan); - - this.inputBPM = document.createElement("input"); - this.inputBPM.value = editor.songData.beatsPerMinute; - this.inputBPM.style.width = "30px"; - this.mainLayout.cells[0][0].appendChild(this.inputBPM); - - this.mainLayout.cells[0][0].appendChild(document.createElement("br")); - this.mainLayout.cells[0][0].appendChild(document.createElement("br")); - - this.buttonSave = document.createElement("button"); - this.buttonSave.innerHTML = "Generate JSON"; - this.buttonSave.className = "toolboxButton"; - this.mainLayout.cells[0][0].appendChild(this.buttonSave); - - this.buttonLoad = document.createElement("button"); - this.buttonLoad.innerHTML = "Load JSON"; - this.buttonLoad.className = "toolboxButton"; - this.mainLayout.cells[0][0].appendChild(this.buttonLoad); - - this.mainLayout.cells[0][0].appendChild(document.createElement("br")); - - this.inputSave = document.createElement("textarea"); - this.inputSave.style.width = "200px"; - this.inputSave.style.height = "100px"; - this.mainLayout.cells[0][0].appendChild(this.inputSave); - - // Key/Meter table layout. - this.keyMeterLayout = this.createTable(this.mainLayout.cells[0][1], 2, 1); - - // Key Change settings. - this.keyMeterLayout.cells[0][0].style.background = "#cccccc"; - this.keyMeterLayout.cells[0][0].style.padding = "10px"; - - var keyChangeEditingLabelSpan = document.createElement("span"); - keyChangeEditingLabelSpan.innerHTML = "Key"; - keyChangeEditingLabelSpan.className = "toolboxLabel"; - this.keyMeterLayout.cells[0][0].appendChild(keyChangeEditingLabelSpan); - this.keyMeterLayout.cells[0][0].appendChild(document.createElement("br")); - - this.keyChangeTonicSelect = document.createElement("select"); - this.keyChangeTonicOptions = []; - for (var i = 0; i < 12; i++) - { - this.keyChangeTonicOptions[i] = document.createElement("option"); - this.keyChangeTonicSelect.appendChild(this.keyChangeTonicOptions[i]); - } - this.keyMeterLayout.cells[0][0].appendChild(this.keyChangeTonicSelect); - - var keyChangeDividerSpan = document.createElement("span"); - keyChangeDividerSpan.innerHTML = " "; - keyChangeDividerSpan.className = "toolboxText"; - this.keyMeterLayout.cells[0][0].appendChild(keyChangeDividerSpan); - - this.keyChangeScaleSelect = document.createElement("select"); - this.keyChangeScaleOptions = []; - for (var i = 0; i < theory.scales.length; i++) - { - this.keyChangeScaleOptions[i] = document.createElement("option"); - this.keyChangeScaleOptions[i].innerHTML = theory.scales[i].name; - this.keyChangeScaleSelect.appendChild(this.keyChangeScaleOptions[i]); - } - this.keyMeterLayout.cells[0][0].appendChild(this.keyChangeScaleSelect); - - this.buttonKeyChangeListen = document.createElement("button"); - this.buttonKeyChangeListen.innerHTML = "Listen"; - this.buttonKeyChangeListen.className = "toolboxButton"; - this.keyMeterLayout.cells[0][0].appendChild(this.buttonKeyChangeListen); - - this.keyMeterLayout.cells[0][0].appendChild(document.createElement("br")); - - this.buttonAddKeyChange = document.createElement("button"); - this.buttonAddKeyChange.innerHTML = "Insert Key Change"; - this.buttonAddKeyChange.className = "toolboxButton"; - this.keyMeterLayout.cells[0][0].appendChild(this.buttonAddKeyChange); - - - // Meter Change settings. - this.keyMeterLayout.cells[0][1].style.background = "#aaddff"; - this.keyMeterLayout.cells[0][1].style.padding = "10px"; - - var meterChangeEditingLabelSpan = document.createElement("span"); - meterChangeEditingLabelSpan.innerHTML = "Meter"; - meterChangeEditingLabelSpan.className = "toolboxLabel"; - this.keyMeterLayout.cells[0][1].appendChild(meterChangeEditingLabelSpan); - this.keyMeterLayout.cells[0][1].appendChild(document.createElement("br")); - - this.meterChangeNumeratorSelect = document.createElement("select"); - this.meterChangeNumeratorOptions = []; - for (var i = 0; i < 20; i++) - { - this.meterChangeNumeratorOptions[i] = document.createElement("option"); - this.meterChangeNumeratorOptions[i].innerHTML = "" + (i + 1); - this.meterChangeNumeratorSelect.appendChild(this.meterChangeNumeratorOptions[i]); - } - this.keyMeterLayout.cells[0][1].appendChild(this.meterChangeNumeratorSelect); - - var meterChangeDividerSpan = document.createElement("span"); - meterChangeDividerSpan.innerHTML = " / "; - meterChangeDividerSpan.className = "toolboxText"; - this.keyMeterLayout.cells[0][1].appendChild(meterChangeDividerSpan); - - this.meterChangeDenominatorSelect = document.createElement("select"); - this.meterChangeDenominatorOptions = []; - this.meterChangeDenominators = [ 2, 4, 8, 16 ]; - for (var i = 0; i < this.meterChangeDenominators.length; i++) - { - this.meterChangeDenominatorOptions[i] = document.createElement("option"); - this.meterChangeDenominatorOptions[i].innerHTML = "" + this.meterChangeDenominators[i]; - this.meterChangeDenominatorSelect.appendChild(this.meterChangeDenominatorOptions[i]); - } - this.keyMeterLayout.cells[0][1].appendChild(this.meterChangeDenominatorSelect); - - this.keyMeterLayout.cells[0][1].appendChild(document.createElement("br")); - - this.buttonAddMeterChange = document.createElement("button"); - this.buttonAddMeterChange.innerHTML = "Insert Meter Change"; - this.buttonAddMeterChange.className = "toolboxButton"; - this.keyMeterLayout.cells[0][1].appendChild(this.buttonAddMeterChange); - - - // Note buttons. - this.notesSpan = document.createElement("span"); - - this.buttonNotes = []; - for (var i = 0; i < 12; i++) - { - this.buttonNotes[i] = document.createElement("button"); - this.buttonNotes[i].className = "toolboxNoteButton"; - this.notesSpan.appendChild(this.buttonNotes[i]); - } - - this.notesSpan.appendChild(document.createElement("br")); - this.notesSpan.appendChild(document.createElement("br")); - this.mainLayout.cells[0][1].appendChild(this.notesSpan); - - - // Chord buttons. - this.chordsSpan = document.createElement("span"); - - this.chordSelect = document.createElement("select"); - this.chordsSpan.appendChild(this.chordSelect); - this.chordsSpan.appendChild(document.createElement("br")); - - this.chordOptions = []; - for (var i = 0; i < theory.chords.length + 1; i++) - { - this.chordOptions[i] = document.createElement("option"); - - var text; - if (i == 0) - { - text = "In Key"; - } - else - { - text = theory.chords[i - 1].roman.replace("X", "I").replace("x", "i"); - text += "" + theory.chords[i - 1].romanSup + ""; - text += "" + theory.chords[i - 1].romanSub + ""; - } - - this.chordOptions[i].innerHTML = text; - this.chordSelect.appendChild(this.chordOptions[i]); - } - - this.buttonChords = []; - for (var i = 0; i < 12; i++) - { - this.buttonChords[i] = document.createElement("button"); - this.buttonChords[i].className = "toolboxNoteChord"; - this.chordsSpan.appendChild(this.buttonChords[i]); - - if (i == 6) - this.chordsSpan.appendChild(document.createElement("br")); - } - - this.mainLayout.cells[0][1].appendChild(this.chordsSpan); + this.prepareKeyPanel(); + this.prepareMeterPanel(); + this.prepareChordsPanel(); // Set up callbacks. var that = this; editor.addOnCursorChanged(function() { that.refresh(); }); editor.addOnSelectionChanged(function() { that.refresh(); }); - this.chordSelect.onchange = function() { that.refresh(); }; + /*this.chordSelect.onchange = function() { that.refresh(); }; this.keyChangeTonicSelect.onchange = function() { that.editKeyChange(); }; this.keyChangeScaleSelect.onchange = function() { that.editKeyChange(); }; this.meterChangeNumeratorSelect.onchange = function() { that.editMeterChange(); }; @@ -354,37 +158,7 @@ function Toolbox(div, editor, theory, synth) } } - this.refresh(); -} - - -Toolbox.prototype.initCSS = function() -{ - var declarations = document.createTextNode( - ".toolboxButton { margin: 2px; border: 2px solid #aaaaaa; background: #ffffff; font-family: tahoma; font-size: 12px; outline: none; }" + - ".toolboxButton:hover { border-color: #cccccc; cursor: pointer; }" + - ".toolboxButton:active { border-color: #aaaaaa; background: #eeeeee; }" + - ".toolboxNoteButton { width: 20px; height: 30px; margin: 2px; padding: 0px; border: none; text-align: center; font-family: tahoma; font-size: 12px; outline: none; }" + - ".toolboxNoteButton:hover { cursor: pointer; border: 1px solid #ffffff; }" + - ".toolboxNoteButton:active { border: 2px solid #ffffff; }" + - ".toolboxNoteChord { width: 50px; height: 38px; margin: 2px; padding: 0px; border-style: solid; border-width: 3px 0 3px 0; background: #eeeeee; text-align: center; font-family: tahoma; font-size: 18px; outline: none; }" + - ".toolboxNoteChord:hover { cursor: pointer; background: #f8f8f8; }" + - ".toolboxNoteChord:active { background: #f0f0f0; }" + - ".toolboxLabel { font-family: tahoma; font-size: 14px; font-weight: bold; }" + - ".toolboxText { font-family: tahoma; font-size: 12px; }" + - ".toolboxPalette { overflow-y: scroll; }"); - - var head = document.getElementsByTagName("head")[0]; - var styleElem = document.createElement("style"); - - styleElem.type = "text/css"; - - if (styleElem.styleSheet) - styleElem.styleSheet.cssText = declarations.nodeValue; - else - styleElem.appendChild(declarations); - - head.appendChild(styleElem); + this.refresh();*/ } @@ -416,17 +190,102 @@ Toolbox.prototype.createTable = function(parent, columns, rows) } -Toolbox.prototype.refresh = function() +Toolbox.prototype.prepareKeyPanel = function() { - if (this.playing) - return; + var keyTonicSelect = document.getElementById("toolboxKeyTonicSelect"); + this.keyTonicOptions = []; + for (var i = 0; i < 12; i++) + { + this.keyTonicOptions[i] = document.createElement("option"); + keyTonicSelect.appendChild(this.keyTonicOptions[i]); + } + + var keyScaleSelect = document.getElementById("toolboxKeyScaleSelect"); + this.keyScaleOptions = []; + for (var i = 0; i < this.theory.scales.length; i++) + { + this.keyScaleOptions[i] = document.createElement("option"); + this.keyScaleOptions[i].innerHTML = this.theory.scales[i].name; + keyScaleSelect.appendChild(this.keyScaleOptions[i]); + } +} + + +Toolbox.prototype.prepareMeterPanel = function() +{ + var meterNumeratorSelect = document.getElementById("toolboxMeterNumeratorSelect"); + this.meterNumeratorOptions = []; + for (var i = 0; i < 20; i++) + { + this.meterNumeratorOptions[i] = document.createElement("option"); + this.meterNumeratorOptions[i].innerHTML = "" + (i + 1); + meterNumeratorSelect.appendChild(this.meterNumeratorOptions[i]); + } + + var meterDenominatorSelect = document.getElementById("toolboxMeterDenominatorSelect"); + this.meterDenominatorOptions = []; + this.meterDenominators = [ 2, 4, 8, 16 ]; + for (var i = 0; i < this.meterDenominators.length; i++) + { + this.meterDenominatorOptions[i] = document.createElement("option"); + this.meterDenominatorOptions[i].innerHTML = "" + this.meterDenominators[i]; + meterDenominatorSelect.appendChild(this.meterDenominatorOptions[i]); + } +} + + +Toolbox.prototype.prepareChordsPanel = function() +{ + var divKinds = document.getElementById("toolboxChordKindsDiv"); + var that = this; + var makeChangeKindFunction = function(index) + { + return function() + { + that.currentChordKind = index; + that.refreshChordsPanel(); + } + } - this.keyMeterLayout.cells[0][0].style.visibility = "hidden"; - this.keyMeterLayout.cells[0][1].style.visibility = "hidden"; + this.chordOptions = []; + for (var i = 0; i < this.theory.chords.length + 1; i++) + { + this.chordOptions[i] = document.createElement("button"); + + var text; + if (i == 0) + text = "In Key"; + else + { + text = this.theory.chords[i - 1].roman.replace("X", "I").replace("x", "i"); + text += "" + this.theory.chords[i - 1].romanSup + ""; + text += "" + this.theory.chords[i - 1].romanSub + ""; + } + + this.chordOptions[i].innerHTML = text; + this.chordOptions[i].className = "toolboxChordKindButton"; + this.chordOptions[i].onclick = makeChangeKindFunction(i); + + divKinds.appendChild(this.chordOptions[i]); + + if (i == 0 || + (i % Math.floor(this.theory.chords.length / 2)) == 0) + { + divKinds.appendChild(document.createElement("br")); + } + } + this.currentChordKind = 0; + this.chordOptions[this.currentChordKind].style.border = "2px solid #ff0000"; +} + + +Toolbox.prototype.refreshKeyPanel = function() +{ + var div = document.getElementById("toolboxKeyDiv"); + div.style.visibility = "hidden"; - // Refresh key change tool. var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); if (keyChange != null) { @@ -440,58 +299,75 @@ Toolbox.prototype.refresh = function() } } - this.keyChangeScaleSelect.selectedIndex = scaleIndex; + document.getElementById("toolboxKeyScaleSelect").selectedIndex = scaleIndex; for (var i = 0; i < 12; i++) { - var pitch = i; - this.keyChangeTonicOptions[i].innerHTML = this.theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); + this.keyTonicOptions[i].innerHTML = this.theory.getNameForPitch(i, keyChange.scale, keyChange.tonicPitch); } - this.keyChangeTonicSelect.selectedIndex = keyChange.tonicPitch; - this.keyMeterLayout.cells[0][0].style.visibility = "visible"; - } - + document.getElementById("toolboxKeyTonicSelect").selectedIndex = keyChange.tonicPitch; + div.style.visibility = "visible"; + } +} + + +Toolbox.prototype.refreshMeterPanel = function() +{ + var div = document.getElementById("toolboxMeterDiv"); + div.style.visibility = "hidden"; - // Refresh meter change tool. var meterChange = this.editor.getMeterAtTick(this.editor.cursorTick); if (meterChange != null) { - this.meterChangeNumeratorSelect.selectedIndex = meterChange.numerator - 1; - if (meterChange.denominator == 2) this.meterChangeDenominatorSelect.selectedIndex = 0; - else if (meterChange.denominator == 4) this.meterChangeDenominatorSelect.selectedIndex = 1; - else if (meterChange.denominator == 8) this.meterChangeDenominatorSelect.selectedIndex = 2; - else if (meterChange.denominator == 16) this.meterChangeDenominatorSelect.selectedIndex = 3; + document.getElementById("toolboxMeterNumeratorSelect").selectedIndex = meterChange.numerator - 1; + + var denominatorIndex = 0; + if (meterChange.denominator == 2) denominatorIndex = 0; + else if (meterChange.denominator == 4) denominatorIndex = 1; + else if (meterChange.denominator == 8) denominatorIndex = 2; + else if (meterChange.denominator == 16) denominatorIndex = 3; + document.getElementById("toolboxMeterDenominatorSelect").selectedIndex = denominatorIndex; - this.keyMeterLayout.cells[0][1].style.visibility = "visible"; + div.style.visibility = "visible"; } - - - // Refresh note tools. +} + + +Toolbox.prototype.refreshNotesPanel = function() +{ + var div = document.getElementById("toolboxNotesDiv"); + div.style.visibility = "hidden"; + + var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); if (keyChange == null) - keyChange = new SongDataKeyChange(0, this.theory.scales[0], this.theory.C); + return; + + div.style.visibility = "visible"; for (var i = 0; i < 12; i++) { + var button = document.getElementById("toolboxNote" + i); + var pitch = (i + keyChange.tonicPitch) % 12; - this.buttonNotes[i].innerHTML = this.theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); + button.innerHTML = this.theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); var degree = this.theory.getDegreeForPitch(pitch, keyChange.scale, keyChange.tonicPitch); if ((degree % 1) == 0) { var color = this.theory.getColorForDegree(degree); - this.buttonNotes[i].style.background = color; - this.buttonNotes[i].style.color = "#000000"; + button.style.background = color; + button.style.color = "#000000"; } else { - this.buttonNotes[i].style.background = "#dddddd"; - this.buttonNotes[i].style.color = "#888888"; + button.style.background = "#dddddd"; + button.style.color = "#888888"; } var that = this; - this.buttonNotes[i].onclick = function(pitch) + button.onclick = function(pitch) { var thisPitch = pitch; return function(ev) @@ -513,13 +389,51 @@ Toolbox.prototype.refresh = function() } }(pitch + 60); } +} + + +Toolbox.prototype.refreshChordsPanel = function() +{ + var div = document.getElementById("toolboxChordsDiv"); + div.style.visibility = "hidden"; + + var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); + if (keyChange == null) + return; + div.style.visibility = "visible"; - // Refresh chord tools. - var chordCategory = this.chordSelect.selectedIndex; + var refreshChordButton = function(theory, button, key, chord, rootPitch) + { + var degree = theory.getDegreeForPitch(rootPitch, key.scale, key.tonicPitch); + var color = theory.getColorForDegree(degree); + var numeral = theory.getRomanNumeralForPitch(rootPitch, key.scale, key.tonicPitch); + + var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); + romanText += "" + chord.romanSup + ""; + romanText += "" + chord.romanSub + ""; - // "In Key" category. - if (chordCategory == 0) + button.innerHTML = romanText; + button.style.borderColor = color; + button.style.visibility = "visible"; + + if ((degree % 1) == 0) + { + button.style.borderColor = color; + button.style.color = "#000000"; + } + else + { + button.style.borderColor = "#dddddd"; + button.style.color = "#888888"; + } + } + + for (var i = 0; i < 12; i++) + document.getElementById("toolboxChord" + i).style.visibility = "hidden"; + + // "In Key" kind. + if (this.currentChordKind == 0) { for (var i = 0; i < 7; i++) { @@ -533,113 +447,69 @@ Toolbox.prototype.refresh = function() if ((i + 4) >= 7) pitch3 += 12; - var color = this.theory.getColorForDegree(i); var chord = this.theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); - var numeral = this.theory.getRomanNumeralForPitch(pitch1, keyChange.scale, keyChange.tonicPitch); - var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); - romanText += "" + chord.romanSup + ""; - romanText += "" + chord.romanSub + ""; - - this.buttonChords[i].innerHTML = romanText; - this.buttonChords[i].style.borderColor = color; - this.buttonChords[i].style.display = "visible"; - - var that = this; - this.buttonChords[i].onclick = function(chord, pitch) - { - var thisChord = chord; - var thisPitch = pitch; - return function(ev) - { - if (that.playing) - return; - - ev.preventDefault(); - that.theory.playChordSample(that.synth, thisChord, thisPitch); - var chord = new SongDataChord(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 2, thisChord, thisPitch); - that.editor.songData.addChord(chord); - that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 2; - that.editor.showCursor = true; - that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - } - }(chord, pitch1); + refreshChordButton(this.theory, document.getElementById("toolboxChord" + i), keyChange, chord, pitch1); } - - for (var i = 7; i < 12; i++) - this.buttonChords[i].style.display = "none"; } - - // Other chord categories. + // Other chord kinds. else - { + { var inKeyIndex = 0; var outOfKeyIndex = 7; + var buttonIndices = [0, 7, 1, 8, 2, 3, 9, 4, 10, 5, 11, 6]; for (var i = 0; i < 12; i++) { var rootPitch = (keyChange.tonicPitch + i) % 12; - var degree = this.theory.getDegreeForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); - - var index = inKeyIndex; - if ((degree % 1) != 0) - index = outOfKeyIndex; + var chord = this.theory.chords[this.currentChordKind - 1]; - var color = this.theory.getColorForDegree(degree); - var chord = this.theory.chords[chordCategory - 1]; - var numeral = this.theory.getRomanNumeralForPitch(rootPitch, keyChange.scale, keyChange.tonicPitch); - - var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); - romanText += "" + chord.romanSup + ""; - romanText += "" + chord.romanSub + ""; - - this.buttonChords[index].innerHTML = romanText; - this.buttonChords[index].style.display = "inline"; - if ((degree % 1) == 0) - { - this.buttonChords[index].style.borderColor = color; - this.buttonChords[index].style.color = "#000000"; - } - else - { - this.buttonChords[index].style.borderColor = "#dddddd"; - this.buttonChords[index].style.color = "#888888"; - } - - var that = this; - this.buttonChords[index].onclick = function(chord, pitch) - { - var thisChord = chord; - var thisPitch = pitch; - return function(ev) - { - if (that.playing) - return; - - ev.preventDefault(); - that.theory.playChordSample(that.synth, thisChord, thisPitch); - var chord = new SongDataChord(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 2, thisChord, thisPitch); - that.editor.songData.addChord(chord); - that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 2; - that.editor.showCursor = true; - that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - } - }(chord, rootPitch); - - if ((degree % 1) != 0) - outOfKeyIndex++; - else - inKeyIndex++; + refreshChordButton(this.theory, document.getElementById("toolboxChord" + buttonIndices[i]), keyChange, chord, rootPitch); } } + + for (var i = 0; i < this.chordOptions.length; i++) + this.chordOptions[i].style.border = "2px solid #ffffff"; + + this.chordOptions[this.currentChordKind].style.border = "2px solid #ff0000"; + + /* + var that = this; + this.buttonChords[i].onclick = function(chord, pitch) + { + var thisChord = chord; + var thisPitch = pitch; + return function(ev) + { + if (that.playing) + return; + + ev.preventDefault(); + that.theory.playChordSample(that.synth, thisChord, thisPitch); + var chord = new SongDataChord(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 2, thisChord, thisPitch); + that.editor.songData.addChord(chord); + that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 2; + that.editor.showCursor = true; + that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; + that.editor.unselectAll(); + that.editor.refreshRepresentation(); + that.editor.refreshCanvas(); + that.refresh(); + } + }(chord, pitch1); + */ +} + + +Toolbox.prototype.refresh = function() +{ + if (this.playing) + return; + + this.refreshKeyPanel(); + this.refreshMeterPanel(); + this.refreshNotesPanel(); + this.refreshChordsPanel(); } diff --git a/toolbox.css b/toolbox.css new file mode 100644 index 0000000..cc21c2d --- /dev/null +++ b/toolbox.css @@ -0,0 +1,111 @@ +.toolboxHeader +{ + font-family: tahoma; + font-size: 14px; + font-weight: bold; +} + +.toolboxLabel +{ + font-family: tahoma; + font-size: 12px; +} + +.toolboxButton +{ + margin: 2px; + border: 2px solid #aaaaaa; + background: #ffffff; + font-family: tahoma; + font-size: 12px; + outline: none; +} + +.toolboxButton:hover +{ + border-color: #cccccc; + cursor: pointer; +} + +.toolboxButton:active +{ + border-color: #aaaaaa; + background: #eeeeee; +} + +.toolboxNoteButton +{ + width: 20px; + height: 30px; + margin: 2px; + padding: 0px; + border: none; + text-align: center; + font-family: tahoma; + font-size: 12px; + outline: none; +} + +.toolboxNoteButton:hover +{ + cursor: pointer; + border: 1px solid #ffffff; +} + +.toolboxNoteButton:active +{ + border: 2px solid #ffffff; +} + +.toolboxChordButton +{ + width: 50px; + height: 38px; + margin: 2px; + padding: 0px; + border-style: solid; + border-width: 3px 0 3px 0; + background: #eeeeee; + text-align: center; + font-family: tahoma; + font-size: 18px; + outline: none; + overflow: hidden; +} + +.toolboxChordButton:hover +{ + cursor: pointer; + background: #f8f8f8; +} + +.toolboxChordButton:active +{ + background: #f0f0f0; +} + +.toolboxChordKindButton +{ + border: 2px solid #ffffff; + background: #ffffff; + font-family: tahoma; + font-size: 12px; + outline: none; + min-width: 30px; +} + +.toolboxChordKindButton:hover +{ + background: #eeeeee; + cursor: pointer; +} + +.toolboxChordKindButton:active +{ + background: #cccccc; +} + +.toolboxPalette +{ + overflow-y: scroll; +} \ No newline at end of file From 6506aa433e8cd930740edf485719cff43766d13f Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Wed, 27 Apr 2016 23:03:08 -0300 Subject: [PATCH 27/46] WIP: re-add toolbox features; add chord embelishments --- index.html | 47 ++--- src/editor_drawing.js | 10 +- src/theory.js | 126 ++++++++++---- src/toolbox.js | 389 +++++++++++++++++++++++------------------- 4 files changed, 332 insertions(+), 240 deletions(-) diff --git a/index.html b/index.html index f170ef5..67f71f9 100644 --- a/index.html +++ b/index.html @@ -15,9 +15,9 @@
- +
- +
Tempo @@ -25,23 +25,23 @@
- Key + Current Key
- +
- +
- Meter + Current Meter
/
- +
@@ -61,20 +61,25 @@
-
-

+
+
+
+
+
+
+
diff --git a/src/editor_drawing.js b/src/editor_drawing.js index ffdcbcd..173dd68 100644 --- a/src/editor_drawing.js +++ b/src/editor_drawing.js @@ -258,15 +258,15 @@ SongEditor.prototype.drawChord = function(region, chord, tick, duration, hoverin // Draw roman symbol. var numeral = this.theory.getRomanNumeralForPitch(chord.rootPitch, region.key.scale, region.key.tonicPitch); - var romanText = chord.chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); + var romanText = (chord.chord.uppercase ? numeral : numeral.toLowerCase()); this.ctx.fillStyle = "#000000"; this.ctx.textAlign = "center"; this.ctx.textBaseline = "middle"; this.ctx.font = "20px Tahoma"; - var supTextWidth = this.ctx.measureText(chord.chord.romanSup).width; - var subTextWidth = this.ctx.measureText(chord.chord.romanSub).width; + var supTextWidth = this.ctx.measureText(chord.chord.symbolSup).width; + var subTextWidth = this.ctx.measureText(chord.chord.symbolSub).width; this.ctx.font = "30px Tahoma"; var mainTextWidth = this.ctx.measureText(romanText).width; @@ -285,8 +285,8 @@ SongEditor.prototype.drawChord = function(region, chord, tick, duration, hoverin this.ctx.fillText(romanText, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth / 2, (pos.y1 + pos.y2) / 2, maxTextWidth - supTextWidth - subTextWidth); this.ctx.font = "20px Tahoma"; - this.ctx.fillText(chord.chord.romanSup, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, (pos.y1 + pos.y2) / 2 - 10, maxTextWidth - mainTextWidth - subTextWidth); - this.ctx.fillText(chord.chord.romanSub, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth + subTextWidth / 2, (pos.y1 + pos.y2) / 2 + 10, maxTextWidth - mainTextWidth - supTextWidth); + this.ctx.fillText(chord.chord.symbolSup, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, (pos.y1 + pos.y2) / 2 - 10, maxTextWidth - mainTextWidth - subTextWidth); + this.ctx.fillText(chord.chord.symbolSub, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth + subTextWidth / 2, (pos.y1 + pos.y2) / 2 + 10, maxTextWidth - mainTextWidth - supTextWidth); // Draw highlights. this.ctx.save(); diff --git a/src/theory.js b/src/theory.js index 4c71a11..652f9aa 100644 --- a/src/theory.js +++ b/src/theory.js @@ -31,27 +31,30 @@ function Theory() // Scale without pitches will be generated below. + this.scaleGroups = + [ "Modes of Major", "Modes of Double Harmonic", "Modes of Phrygian Dominant" ]; + this.scales = [ - { name: "Major", pitches: [ C, D, E, F, G, A, B ] }, - { name: "Dorian", pitches: [] }, - { name: "Phrygian", pitches: [] }, - { name: "Lydian", pitches: [] }, - { name: "Mixolydian", pitches: [] }, - { name: "Natural Minor", pitches: [] }, - { name: "Locrian", pitches: [] }, + { name: "Major", group: 0, pitches: [ C, D, E, F, G, A, B ] }, + { name: "Dorian", group: 0, pitches: [] }, + { name: "Phrygian", group: 0, pitches: [] }, + { name: "Lydian", group: 0, pitches: [] }, + { name: "Mixolydian", group: 0, pitches: [] }, + { name: "Natural Minor", group: 0, pitches: [] }, + { name: "Locrian", group: 0, pitches: [] }, - { name: "Harmonic Minor", pitches: [ C, D, Ds, F, G, Gs, B ] }, + { name: "Harmonic Minor", group: 0, pitches: [ C, D, Ds, F, G, Gs, B ] }, - { name: "Double Harmonic", pitches: [ C, Cs, E, F, G, Gs, B ] }, - { name: "Lydian ♯2 ♯6", pitches: [] }, - { name: "Ultraphrygian", pitches: [] }, - { name: "Hungarian Minor", pitches: [] }, - { name: "Oriental", pitches: [] }, - { name: "Ionian Augmented ♯2", pitches: [] }, - { name: "Locrian ♭♭3 ♭♭7", pitches: [] }, + { name: "Double Harmonic", group: 1, pitches: [ C, Cs, E, F, G, Gs, B ] }, + { name: "Lydian ♯2 ♯6", group: 1, pitches: [] }, + { name: "Ultraphrygian", group: 1, pitches: [] }, + { name: "Hungarian Minor", group: 1, pitches: [] }, + { name: "Oriental", group: 1, pitches: [] }, + { name: "Ionian Augmented ♯2", group: 1, pitches: [] }, + { name: "Locrian ♭♭3 ♭♭7", group: 1, pitches: [] }, - { name: "Phrygian Dominant", pitches: [ C, Cs, E, F, G, Gs, As ] }, + { name: "Phrygian Dominant", group: 2, pitches: [ C, Cs, E, F, G, Gs, As ] }, ]; // Generate the other modes of the non-null scales. @@ -72,7 +75,7 @@ function Theory() { this.scales[i].pitchToDegreeMap = []; - var curPitch = 0; + var curPitch = 0; for (var j = 0; j < 7; j++) { while (curPitch < this.scales[i].pitches[j]) @@ -84,25 +87,39 @@ function Theory() this.scales[i].pitchToDegreeMap.push(j); curPitch++; } + + while (curPitch < 12) + { + this.scales[i].pitchToDegreeMap.push(6.5); + curPitch++; + } } this.chords = [ - { name: "Major", roman: "X", romanSup: "", romanSub: "", pitches: [ C, E, G ] }, - { name: "Minor", roman: "x", romanSup: "", romanSub: "", pitches: [ C, Ds, G ] }, - { name: "Diminished", roman: "x", romanSup: "o", romanSub: "", pitches: [ C, Ds, Fs ] }, - { name: "Augmented", roman: "X", romanSup: "+", romanSub: "", pitches: [ C, E, Gs ] }, - { name: "Flat Fifth(?)", roman: "X", romanSup: "(♭5)", romanSub: "", pitches: [ C, E, Fs ] }, - { name: "?", roman: "X", romanSup: "?", romanSub: "", pitches: [ C, D, Fs ] }, - { name: "Dominant Seventh", roman: "X", romanSup: "7", romanSub: "", pitches: [ C, E, G, As ] }, - { name: "Major Seventh", roman: "X", romanSup: "M7", romanSub: "", pitches: [ C, E, G, B ] }, - { name: "Minor Seventh", roman: "x", romanSup: "7", romanSub: "", pitches: [ C, Ds, G, As ] }, - { name: "Minor-Major Seventh", roman: "X", romanSup: "m(M7)", romanSub: "", pitches: [ C, Ds, G, B ] }, - { name: "Diminished Seventh", roman: "x", romanSup: "o7", romanSub: "", pitches: [ C, Ds, G, A ] }, - { name: "Half-Diminished Seventh", roman: "x", romanSup: "ø7", romanSub: "", pitches: [ C, Ds, Fs, As ] }, - { name: "Augmented Seventh", roman: "X", romanSup: "+7", romanSub: "", pitches: [ C, E, Gs, As ] }, - { name: "Augmented Major Seventh", roman: "X", romanSup: "+(M7)", romanSub: "", pitches: [ C, E, Gs, B ] } + { name: "Major", uppercase: true, symbolSup: "", symbolSub: "", pitches: [ C, E, G ] }, + { name: "Minor", uppercase: false, symbolSup: "", symbolSub: "", pitches: [ C, Ds, G ] }, + { name: "Diminished", uppercase: false, symbolSup: "o", symbolSub: "", pitches: [ C, Ds, Fs ] }, + { name: "Augmented", uppercase: true, symbolSup: "+", symbolSub: "", pitches: [ C, E, Gs ] }, + { name: "Flat Fifth(?)", uppercase: true, symbolSup: "(♭5)", symbolSub: "", pitches: [ C, E, Fs ] }, + { name: "?", uppercase: true, symbolSup: "?", symbolSub: "", pitches: [ C, D, Fs ] }, + { name: "Dominant Seventh", uppercase: true, symbolSup: "7", symbolSub: "", pitches: [ C, E, G, As ] }, + { name: "Major Seventh", uppercase: true, symbolSup: "M7", symbolSub: "", pitches: [ C, E, G, B ] }, + { name: "Minor Seventh", uppercase: false, symbolSup: "7", symbolSub: "", pitches: [ C, Ds, G, As ] }, + { name: "Minor-Major Seventh", uppercase: true, symbolSup: "m(M7)", symbolSub: "", pitches: [ C, Ds, G, B ] }, + { name: "Diminished Seventh", uppercase: false, symbolSup: "o7", symbolSub: "", pitches: [ C, Ds, G, A ] }, + { name: "Half-Diminished Seventh", uppercase: false, symbolSup: "ø7", symbolSub: "", pitches: [ C, Ds, Fs, As ] }, + { name: "Augmented Seventh", uppercase: true, symbolSup: "+7", symbolSub: "", pitches: [ C, E, Gs, As ] }, + { name: "Augmented Major Seventh", uppercase: true, symbolSup: "+(M7)", symbolSub: "", pitches: [ C, E, Gs, B ] } + ]; + + + this.chordEmbelishments = + [ + { name: "Suspended Second", symbol: "sus2" }, + { name: "Suspended Fourth", symbol: "sus4" }, + { name: "Added Ninth", symbol: "add9" } ]; @@ -208,7 +225,6 @@ function Theory() if (tonicPitch != 0) offset = Math.floor(this.getRowForPitch(tonicPitch, scale, 0)); - return pitchDegree + offset + pitchOctave * 7; }; @@ -226,8 +242,48 @@ function Theory() }; - // Returns the first chord in the chord array that fits the given pitches. - this.getFirstFittingChordForPitches = function(pitches) + // Returns a new chord altered by the given embelishments. + this.getEmbelishedChord = function(chordIndex, embelishmentIndices) + { + var newChord = { + name: this.chords[chordIndex].name, + uppercase: this.chords[chordIndex].uppercase, + symbolSup: this.chords[chordIndex].symbolSup, + symbolSub: this.chords[chordIndex].symbolSub, + pitches: this.chords[chordIndex].pitches.slice(0) + }; + + var hasSus2 = (embelishmentIndices.indexOf(0) >= 0); + var hasSus4 = (embelishmentIndices.indexOf(1) >= 0); + var hasAdd9 = (embelishmentIndices.indexOf(2) >= 0); + + if (hasAdd9) + { + newChord.pitches.splice(1, 0, D); + newChord.symbolSup += "(add9)"; + } + else if (hasSus2 && hasSus4) + { + newChord.pitches.splice(1, 1, D, F); + newChord.symbolSub += "sus24"; + } + else if (hasSus2) + { + newChord.pitches[1] = D; + newChord.symbolSub += "sus2"; + } + else if (hasSus4) + { + newChord.pitches[1] = F; + newChord.symbolSub += "sus4"; + } + + return newChord; + } + + + // Returns the first chord index in the chord array that fits the given pitches. + this.getFirstFittingChordIndexForPitches = function(pitches) { for (var i = 0; i < this.chords.length; i++) { @@ -248,7 +304,7 @@ function Theory() } if (match) - return chord; + return i; } console.log("Missing chord data for pitches:"); diff --git a/src/toolbox.js b/src/toolbox.js index e93a23c..d16ef98 100644 --- a/src/toolbox.js +++ b/src/toolbox.js @@ -11,6 +11,7 @@ function Toolbox(div, editor, theory, synth) this.currentChordKind = 0; + this.prepareControlsPanel(); this.prepareKeyPanel(); this.prepareMeterPanel(); this.prepareChordsPanel(); @@ -20,97 +21,10 @@ function Toolbox(div, editor, theory, synth) var that = this; editor.addOnCursorChanged(function() { that.refresh(); }); editor.addOnSelectionChanged(function() { that.refresh(); }); - /*this.chordSelect.onchange = function() { that.refresh(); }; - this.keyChangeTonicSelect.onchange = function() { that.editKeyChange(); }; - this.keyChangeScaleSelect.onchange = function() { that.editKeyChange(); }; - this.meterChangeNumeratorSelect.onchange = function() { that.editMeterChange(); }; - this.meterChangeDenominatorSelect.onchange = function() { that.editMeterChange(); }; - this.buttonKeyChangeListen.onclick = function() - { - var keyChange = that.editor.getKeyAtTick(that.editor.cursorTick); - that.theory.playScaleSample(that.synth, keyChange.scale, keyChange.tonicPitch); - } - - this.buttonAddKeyChange.onclick = function(ev) - { - if (that.playing) - return; - - ev.preventDefault(); - var keyChange = new SongDataKeyChange(that.editor.cursorTick, theory.scales[0], theory.C); - that.editor.songData.addKeyChange(keyChange); - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.selectKeyChange(keyChange); - that.editor.refreshCanvas(); - that.refresh(); - }; - - this.buttonAddMeterChange.onclick = function(ev) - { - if (that.playing) - return; - - ev.preventDefault(); - var meterChange = new SongDataMeterChange(that.editor.cursorTick, 4, 4); - that.editor.songData.addMeterChange(meterChange); - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.selectMeterChange(meterChange); - that.editor.refreshCanvas(); - that.refresh(); - }; - - this.buttonPlay.onclick = function() - { - that.playing = !that.playing; - that.editor.setInteractionEnabled(!that.playing); - that.buttonPlay.innerHTML = (that.playing ? "■ Stop" : "▶ Play"); - - that.editor.cursorTick = Math.floor(that.editor.cursorTick / that.editor.tickSnap) * that.editor.tickSnap; - that.editor.showCursor = true; - that.editor.unselectAll(); - that.editor.clearHover(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - - that.synth.stopAll(); - if (that.playing) - { - that.editor.cursorZone = that.editor.CURSOR_ZONE_ALL; - that.playTimer = setInterval(function() { that.processPlayback(); }, 1000 / 60); - that.playTimerRefresh = setInterval(function() { that.processPlaybackRefresh(); }, 1000 / 15); - } - else - { - clearInterval(that.playTimer); - clearInterval(that.playTimerRefresh); - } - } - - this.buttonRewind.onclick = function() - { - if (that.playing) - { - clearInterval(that.playTimer); - clearInterval(that.playTimerRefresh); - that.synth.stopAll(); - } - - that.playing = false; - that.editor.setInteractionEnabled(true); - that.buttonPlay.innerHTML = (that.playing ? "■ Stop" : "▶ Play"); - - that.editor.cursorTick = 0; - that.editor.showCursor = true; - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - } + this.refresh(); + /* this.inputBPM.onchange = function() { var bpm = parseInt(that.inputBPM.value); @@ -156,37 +70,15 @@ function Toolbox(div, editor, theory, synth) that.editor.refreshCanvas(); that.refresh(); } - } - - this.refresh();*/ + }*/ } -Toolbox.prototype.createTable = function(parent, columns, rows) +Toolbox.prototype.prepareControlsPanel = function() { - var table = {}; - table.root = document.createElement("table"); - table.root.style.margin = "auto"; - - table.rows = []; - table.cells = []; - - for (var j = 0; j < rows; j++) - { - table.cells[j] = []; - - table.rows[j] = document.createElement("tr"); - table.root.appendChild(table.rows[j]); - - for (var i = 0; i < columns; i++) - { - table.cells[j][i] = document.createElement("td"); - table.rows[j].appendChild(table.cells[j][i]); - } - } - - parent.appendChild(table.root); - return table; + var that = this; + document.getElementById("toolboxPlayButton").onclick = function() { that.togglePlay(); }; + document.getElementById("toolboxRewindButton").onclick = function() { that.rewind(); }; } @@ -202,12 +94,28 @@ Toolbox.prototype.prepareKeyPanel = function() var keyScaleSelect = document.getElementById("toolboxKeyScaleSelect"); this.keyScaleOptions = []; + var group = -1; + var groupElement = null; for (var i = 0; i < this.theory.scales.length; i++) { + if (group != this.theory.scales[i].group) + { + group = this.theory.scales[i].group; + groupElement = document.createElement("optgroup"); + groupElement.label = this.theory.scaleGroups[group]; + keyScaleSelect.appendChild(groupElement); + } + this.keyScaleOptions[i] = document.createElement("option"); this.keyScaleOptions[i].innerHTML = this.theory.scales[i].name; - keyScaleSelect.appendChild(this.keyScaleOptions[i]); + groupElement.appendChild(this.keyScaleOptions[i]); } + + var that = this; + keyTonicSelect.onchange = function() { that.editKeyChange(); }; + keyScaleSelect.onchange = function() { that.editKeyChange(); }; + document.getElementById("toolboxKeyListenButton").onclick = function() { that.listenToKey(); }; + document.getElementById("toolboxKeyInsertButton").onclick = function() { that.insertKeyChange(); }; } @@ -231,12 +139,18 @@ Toolbox.prototype.prepareMeterPanel = function() this.meterDenominatorOptions[i].innerHTML = "" + this.meterDenominators[i]; meterDenominatorSelect.appendChild(this.meterDenominatorOptions[i]); } + + var that = this; + meterNumeratorSelect.onchange = function() { that.editMeterChange(); }; + meterDenominatorSelect.onchange = function() { that.editMeterChange(); }; + document.getElementById("toolboxMeterInsertButton").onclick = function() { that.insertMeterChange(); }; } Toolbox.prototype.prepareChordsPanel = function() { var divKinds = document.getElementById("toolboxChordKindsDiv"); + var divEmbelishments = document.getElementById("toolboxChordEmbelishDiv"); var that = this; var makeChangeKindFunction = function(index) @@ -258,9 +172,9 @@ Toolbox.prototype.prepareChordsPanel = function() text = "In Key"; else { - text = this.theory.chords[i - 1].roman.replace("X", "I").replace("x", "i"); - text += "" + this.theory.chords[i - 1].romanSup + ""; - text += "" + this.theory.chords[i - 1].romanSub + ""; + text = (this.theory.chords[i - 1].uppercase ? "I" : "i"); + text += "" + this.theory.chords[i - 1].symbolSup + ""; + text += "" + this.theory.chords[i - 1].symbolSub + ""; } this.chordOptions[i].innerHTML = text; @@ -278,6 +192,25 @@ Toolbox.prototype.prepareChordsPanel = function() this.currentChordKind = 0; this.chordOptions[this.currentChordKind].style.border = "2px solid #ff0000"; + + this.chordEmbelishmentOptions = []; + for (var i = 0; i < this.theory.chordEmbelishments.length; i++) + { + var text = this.theory.chordEmbelishments[i].symbol; + + var label = document.createElement("label"); + label.className = "toolboxLabel"; + + this.chordEmbelishmentOptions[i] = document.createElement("input"); + this.chordEmbelishmentOptions[i].type = "checkbox"; + this.chordEmbelishmentOptions[i].onclick = function() { that.refreshChordsPanel(); }; + + label.appendChild(this.chordEmbelishmentOptions[i]); + label.appendChild(document.createTextNode(text)); + divEmbelishments.appendChild(label); + + divEmbelishments.appendChild(document.createElement("br")); + } } @@ -345,6 +278,7 @@ Toolbox.prototype.refreshNotesPanel = function() div.style.visibility = "visible"; + var that = this; for (var i = 0; i < 12; i++) { var button = document.getElementById("toolboxNote" + i); @@ -366,28 +300,7 @@ Toolbox.prototype.refreshNotesPanel = function() button.style.color = "#888888"; } - var that = this; - button.onclick = function(pitch) - { - var thisPitch = pitch; - return function(ev) - { - if (that.playing) - return; - - ev.preventDefault(); - that.theory.playNoteSample(that.synth, thisPitch); - var note = new SongDataNote(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 4, thisPitch); - that.editor.songData.addNote(note); - that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 4; - that.editor.showCursor = true; - that.editor.cursorZone = that.editor.CURSOR_ZONE_NOTES; - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - } - }(pitch + 60); + button.onclick = function(pitch) { var innerPitch = pitch; return function() { that.insertNote(innerPitch); }; }(pitch + 60); } } @@ -403,15 +316,16 @@ Toolbox.prototype.refreshChordsPanel = function() div.style.visibility = "visible"; + var that = this; var refreshChordButton = function(theory, button, key, chord, rootPitch) { var degree = theory.getDegreeForPitch(rootPitch, key.scale, key.tonicPitch); var color = theory.getColorForDegree(degree); var numeral = theory.getRomanNumeralForPitch(rootPitch, key.scale, key.tonicPitch); - var romanText = chord.roman.replace("X", numeral).replace("x", numeral.toLowerCase()); - romanText += "" + chord.romanSup + ""; - romanText += "" + chord.romanSub + ""; + var romanText = (chord.uppercase ? numeral : numeral.toLowerCase()); + romanText += "" + chord.symbolSup + ""; + romanText += "" + chord.symbolSub + ""; button.innerHTML = romanText; button.style.borderColor = color; @@ -427,11 +341,21 @@ Toolbox.prototype.refreshChordsPanel = function() button.style.borderColor = "#dddddd"; button.style.color = "#888888"; } + + button.onclick = function() { that.insertChord(chord, rootPitch); }; } for (var i = 0; i < 12; i++) document.getElementById("toolboxChord" + i).style.visibility = "hidden"; + + var embelishmentIndices = []; + for (var i = 0; i < this.chordEmbelishmentOptions.length; i++) + { + if (this.chordEmbelishmentOptions[i].checked) + embelishmentIndices.push(i); + } + // "In Key" kind. if (this.currentChordKind == 0) { @@ -447,7 +371,8 @@ Toolbox.prototype.refreshChordsPanel = function() if ((i + 4) >= 7) pitch3 += 12; - var chord = this.theory.getFirstFittingChordForPitches([pitch1, pitch2, pitch3]); + var chordIndex = this.theory.getFirstFittingChordIndexForPitches([pitch1, pitch2, pitch3]); + var chord = this.theory.getEmbelishedChord(chordIndex, embelishmentIndices); refreshChordButton(this.theory, document.getElementById("toolboxChord" + i), keyChange, chord, pitch1); } @@ -462,8 +387,10 @@ Toolbox.prototype.refreshChordsPanel = function() for (var i = 0; i < 12; i++) { var rootPitch = (keyChange.tonicPitch + i) % 12; - var chord = this.theory.chords[this.currentChordKind - 1]; + var chordIndex = this.currentChordKind - 1; + var chord = this.theory.getEmbelishedChord(chordIndex, embelishmentIndices); + // FIXME: Coloration for flat/sharp degrees is irregular for different scales. refreshChordButton(this.theory, document.getElementById("toolboxChord" + buttonIndices[i]), keyChange, chord, rootPitch); } } @@ -472,37 +399,13 @@ Toolbox.prototype.refreshChordsPanel = function() this.chordOptions[i].style.border = "2px solid #ffffff"; this.chordOptions[this.currentChordKind].style.border = "2px solid #ff0000"; - - /* - var that = this; - this.buttonChords[i].onclick = function(chord, pitch) - { - var thisChord = chord; - var thisPitch = pitch; - return function(ev) - { - if (that.playing) - return; - - ev.preventDefault(); - that.theory.playChordSample(that.synth, thisChord, thisPitch); - var chord = new SongDataChord(that.editor.cursorTick, that.editor.songData.ticksPerWholeNote / 2, thisChord, thisPitch); - that.editor.songData.addChord(chord); - that.editor.cursorTick += that.editor.songData.ticksPerWholeNote / 2; - that.editor.showCursor = true; - that.editor.cursorZone = that.editor.CURSOR_ZONE_CHORDS; - that.editor.unselectAll(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - } - }(chord, pitch1); - */ } Toolbox.prototype.refresh = function() { + document.getElementById("toolboxPlayButton").innerHTML = (this.playing ? "■ Stop" : "▶ Play"); + if (this.playing) return; @@ -513,6 +416,49 @@ Toolbox.prototype.refresh = function() } +Toolbox.prototype.insertNote = function(pitch) +{ + if (this.playing) + return; + + this.theory.playNoteSample(this.synth, pitch); + var note = new SongDataNote(this.editor.cursorTick, this.editor.songData.ticksPerWholeNote / 4, pitch); + this.editor.songData.addNote(note); + this.editor.cursorTick += this.editor.songData.ticksPerWholeNote / 4; + this.editor.showCursor = true; + this.editor.cursorZone = this.editor.CURSOR_ZONE_NOTES; + this.editor.unselectAll(); + this.editor.refreshRepresentation(); + this.editor.refreshCanvas(); + this.refresh(); +} + + +Toolbox.prototype.insertChord = function(chord, pitch) +{ + if (this.playing) + return; + + this.theory.playChordSample(this.synth, chord, pitch); + var songChord = new SongDataChord(this.editor.cursorTick, this.editor.songData.ticksPerWholeNote / 2, chord, pitch); + this.editor.songData.addChord(songChord); + this.editor.cursorTick += this.editor.songData.ticksPerWholeNote / 2; + this.editor.showCursor = true; + this.editor.cursorZone = this.editor.CURSOR_ZONE_CHORDS; + this.editor.unselectAll(); + this.editor.refreshRepresentation(); + this.editor.refreshCanvas(); + this.refresh(); +} + + +Toolbox.prototype.listenToKey = function() +{ + var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); + this.theory.playScaleSample(this.synth, keyChange.scale, keyChange.tonicPitch); +} + + Toolbox.prototype.editKeyChange = function() { if (this.playing) @@ -522,8 +468,11 @@ Toolbox.prototype.editKeyChange = function() if (keyChange == null) return; - keyChange.scale = this.theory.scales[this.keyChangeScaleSelect.selectedIndex]; - keyChange.tonicPitch = this.keyChangeTonicSelect.selectedIndex; + var keyTonicSelect = document.getElementById("toolboxKeyTonicSelect"); + var keyScaleSelect = document.getElementById("toolboxKeyScaleSelect"); + + keyChange.scale = this.theory.scales[keyScaleSelect.selectedIndex]; + keyChange.tonicPitch = keyTonicSelect.selectedIndex; this.editor.refreshRepresentation(); this.editor.selectKeyChange(keyChange); @@ -532,6 +481,21 @@ Toolbox.prototype.editKeyChange = function() } +Toolbox.prototype.insertKeyChange = function() +{ + if (this.playing) + return; + + var keyChange = new SongDataKeyChange(this.editor.cursorTick, this.theory.scales[0], this.theory.C); + this.editor.songData.addKeyChange(keyChange); + this.editor.unselectAll(); + this.editor.refreshRepresentation(); + this.editor.selectKeyChange(keyChange); + this.editor.refreshCanvas(); + this.refresh(); +} + + Toolbox.prototype.editMeterChange = function() { if (this.playing) @@ -541,8 +505,11 @@ Toolbox.prototype.editMeterChange = function() if (meterChange == null) return; - meterChange.numerator = this.meterChangeNumeratorSelect.selectedIndex + 1; - meterChange.denominator = this.meterChangeDenominators[this.meterChangeDenominatorSelect.selectedIndex]; + var meterNumeratorSelect = document.getElementById("toolboxMeterNumeratorSelect"); + var meterDenominatorSelect = document.getElementById("toolboxMeterDenominatorSelect"); + + meterChange.numerator = meterNumeratorSelect.selectedIndex + 1; + meterChange.denominator = this.meterDenominators[meterDenominatorSelect.selectedIndex]; this.editor.refreshRepresentation(); this.editor.selectMeterChange(meterChange); @@ -551,6 +518,72 @@ Toolbox.prototype.editMeterChange = function() } +Toolbox.prototype.insertMeterChange = function() +{ + if (this.playing) + return; + + var meterChange = new SongDataMeterChange(this.editor.cursorTick, 4, 4); + this.editor.songData.addMeterChange(meterChange); + this.editor.unselectAll(); + this.editor.refreshRepresentation(); + this.editor.selectMeterChange(meterChange); + this.editor.refreshCanvas(); + this.refresh(); +} + + +Toolbox.prototype.togglePlay = function() +{ + this.playing = !this.playing; + this.editor.setInteractionEnabled(!this.playing); + + this.editor.cursorTick = Math.floor(this.editor.cursorTick / this.editor.tickSnap) * this.editor.tickSnap; + this.editor.showCursor = true; + this.editor.unselectAll(); + this.editor.clearHover(); + this.editor.refreshRepresentation(); + this.editor.refreshCanvas(); + this.refresh(); + + this.synth.stopAll(); + if (this.playing) + { + this.editor.cursorZone = this.editor.CURSOR_ZONE_ALL; + + var that = this; + this.playTimer = setInterval(function() { that.processPlayback(); }, 1000 / 60); + this.playTimerRefresh = setInterval(function() { that.processPlaybackRefresh(); }, 1000 / 15); + } + else + { + clearInterval(this.playTimer); + clearInterval(this.playTimerRefresh); + } +} + + +Toolbox.prototype.rewind = function() +{ + if (this.playing) + { + clearInterval(this.playTimer); + clearInterval(this.playTimerRefresh); + this.synth.stopAll(); + } + + this.playing = false; + this.editor.setInteractionEnabled(true); + + this.editor.cursorTick = 0; + this.editor.showCursor = true; + this.editor.unselectAll(); + this.editor.refreshRepresentation(); + this.editor.refreshCanvas(); + this.refresh(); +} + + Toolbox.prototype.processPlayback = function() { var bpm = this.editor.songData.beatsPerMinute; @@ -601,8 +634,6 @@ Toolbox.prototype.processPlayback = function() } - - Toolbox.prototype.processPlaybackRefresh = function() { this.editor.unselectAll(); From e88fbd20908d983a609d9a6e2f772d353a8246d9 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Wed, 17 Aug 2016 22:39:37 -0300 Subject: [PATCH 28/46] start overhaul --- .gitignore | 1 + index.html | 105 +----- main.js | 94 ++--- src/editor.js | 128 ------- src/editor_drawing.js | 574 ------------------------------ src/editor_event_handling.js | 614 -------------------------------- src/editor_interaction.js | 383 -------------------- src/editor_representation.js | 271 --------------- src/song/meter.js | 6 + src/song/note.js | 5 + src/song/song.js | 105 ++++++ src/songdata.js | 542 ----------------------------- src/synth.js | 116 ------- src/theory.js | 342 ------------------ src/timeline/timeline.js | 319 +++++++++++++++++ src/timeline/track_notes.js | 146 ++++++++ src/toolbox.js | 657 ----------------------------------- src/util/callback.js | 27 ++ src/util/dom.js | 9 + src/util/map_by_time.js | 60 ++++ src/util/map_by_timerange.js | 60 ++++ src/util/pitch.js | 10 + src/util/timerange.js | 36 ++ toolbox.css | 111 ------ 24 files changed, 818 insertions(+), 3903 deletions(-) create mode 100644 .gitignore delete mode 100644 src/editor.js delete mode 100644 src/editor_drawing.js delete mode 100644 src/editor_event_handling.js delete mode 100644 src/editor_interaction.js delete mode 100644 src/editor_representation.js create mode 100644 src/song/meter.js create mode 100644 src/song/note.js create mode 100644 src/song/song.js delete mode 100644 src/songdata.js delete mode 100644 src/synth.js delete mode 100644 src/theory.js create mode 100644 src/timeline/timeline.js create mode 100644 src/timeline/track_notes.js delete mode 100644 src/toolbox.js create mode 100644 src/util/callback.js create mode 100644 src/util/dom.js create mode 100644 src/util/map_by_time.js create mode 100644 src/util/map_by_timerange.js create mode 100644 src/util/pitch.js create mode 100644 src/util/timerange.js delete mode 100644 toolbox.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a47f7e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/temp/ \ No newline at end of file diff --git a/index.html b/index.html index 67f71f9..d7e4f84 100644 --- a/index.html +++ b/index.html @@ -5,102 +5,25 @@ Music Analysis - - -
- -
-
- -
- -
- -
- Tempo - - -
- -
- Current Key -
- - - -
- -
- -
- Current Meter -
- - / - -
- -
- -
- -
- -
-
-
-
-
-
-
-
-
- -
-
- -
- -
- + +
+
- - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/main.js b/main.js index 9abf1e1..7e73105 100644 --- a/main.js +++ b/main.js @@ -1,77 +1,23 @@ -var mainEditor; +var mainTimeline; -function testOnLoad() + +function main() { - var theory = new Theory(); - - var song = new SongData(theory); - - song.addKeyChange(new SongDataKeyChange(960 * 2, theory.scales[0], theory.D)); - song.addMeterChange(new SongDataMeterChange(960 * 2, 3, 4)); - song.addSectionBreak(new SongSectionBreak(960 * 1.5)); - - song.addChord(new SongDataChord(960 * 0.0, 960 / 2, theory.chords[0], theory.C)); - song.addChord(new SongDataChord(960 * 0.5, 960 / 2, theory.chords[0], theory.F)); - song.addChord(new SongDataChord(960 * 1.0, 960 / 1, theory.chords[0], theory.G)); - - song.addNote(new SongDataNote(0, 960 / 4, theory.C + 60)); - song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.D + 60)); - song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); - song.addNote(new SongDataNote(960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); - song.addNote(new SongDataNote(960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.G + 60)); - song.addNote(new SongDataNote(960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.A + 60)); - song.addNote(new SongDataNote(960, 960, theory.B + 60)); - - song.addNote(new SongDataNote(960 * 2, 960 / 4, theory.C + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4, 960 / 16, theory.D + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.G + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.A + 60)); - song.addNote(new SongDataNote(960 * 2 + 960, 960 / 2, theory.B + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 + 960 / 2, 960 / 2, theory.C + 72)); - - /*song.addKeyChange(new SongDataKeyChange(0, theory.scales[0], theory.C)); - song.addMeterChange(new SongDataMeterChange(0, 4, 4)); - song.addKeyChange(new SongDataKeyChange(960 * 2, theory.scales[8], theory.C)); - song.addKeyChange(new SongDataKeyChange(960 * 4, theory.scales[0], theory.C)); - - song.addChord(new SongDataChord(960 * 0.0, 960 / 2, theory.chords[0], theory.C)); - song.addChord(new SongDataChord(960 * 0.5, 960 / 2, theory.chords[0], theory.F)); - song.addChord(new SongDataChord(960 * 1.0, 960 / 1, theory.chords[0], theory.G)); - song.addChord(new SongDataChord(960 * 2.0, 960 / 2, theory.chords[0], theory.C)); - song.addChord(new SongDataChord(960 * 2.5, 960 / 2, theory.chords[1], theory.F)); - song.addChord(new SongDataChord(960 * 3.0, 960 / 2, theory.chords[6], theory.C)); - song.addChord(new SongDataChord(960 * 3.5, 960 / 2, theory.chords[4], theory.G)); - song.addChord(new SongDataChord(960 * 4.0, 960 * 2, theory.chords[0], theory.C)); - - song.addNote(new SongDataNote(0, 960 / 4, theory.C + 60)); - song.addNote(new SongDataNote(960 / 4, 960 / 16, theory.F + 60)); - song.addNote(new SongDataNote(960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); - song.addNote(new SongDataNote(960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); - song.addNote(new SongDataNote(960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E + 60)); - song.addNote(new SongDataNote(960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.D + 60)); - song.addNote(new SongDataNote(960, 960, theory.G + 60)); - - song.addNote(new SongDataNote(960 * 2, 960 / 4, theory.C + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4, 960 / 16, theory.F + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16, 960 / 16, theory.E + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 + 960 / 16 * 2, 960 / 4, theory.F + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 2 + 960 / 16 * 2, 960 / 4, theory.E + 60)); - song.addNote(new SongDataNote(960 * 2 + 960 / 4 * 3 + 960 / 16 * 2, 960 / 8, theory.Cs + 60)); - song.addNote(new SongDataNote(960 * 2 + 960, 960, theory.C + 60)); - - song.addNote(new SongDataNote(960 * 2.5, 960 / 16, theory.Gs + 60)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16, 960 / 16, theory.C + 60 + 12)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 2, 960 / 16, theory.Cs + 60 + 12)); - song.addNote(new SongDataNote(960 * 2.5 + 960 / 16 * 3, 960 / 16, theory.E + 60 + 12));*/ - - var synth = new Synth(); - setInterval(function() { synth.process(6); }, 1000 / 60); - - var canvas = document.getElementById("editorCanvas"); - var editor = new SongEditor(canvas, theory, synth, song); - editor.refreshCanvas(); - mainEditor = editor; - var toolbox = new Toolbox(document.getElementById("toolboxDiv"), editor, theory, synth); + var div = document.getElementById("divTimeline"); + var canvas = document.getElementById("canvasTimeline"); + canvas.width = div.clientWidth; + + var song = new Song(); + song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(3 * 12 + 0))); + song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(3 * 12 + 1))); + song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(3 * 12 + 2))); + song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(3 * 12 + 3))); + song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(3 * 12 + 4))); + + var timeline = new Timeline(canvas); + timeline.setSong(song); + timeline.relayout(); + timeline.redraw(); + + mainTimeline = timeline; } \ No newline at end of file diff --git a/src/editor.js b/src/editor.js deleted file mode 100644 index 423fad9..0000000 --- a/src/editor.js +++ /dev/null @@ -1,128 +0,0 @@ -function SongEditor(canvas, theory, synth, songData) -{ - this.canvas = canvas; - this.ctx = canvas.getContext("2d"); - this.canvasWidth = parseFloat(canvas.width); - this.canvasHeight = parseFloat(canvas.height); - - // Set up mouse/keyboard interaction. - var that = this; - this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; - this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; - this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; - window.onkeydown = function(ev) { that.handleKeyDown(ev); }; - - // Set up callback arrays. - this.cursorChangedCallbacks = []; - this.selectionChangedCallbacks = []; - - // For each object in the song data, these arrays store a boolean - // indicating whether the respective object is currently selected in the editor. - this.noteSelections = []; - this.chordSelections = []; - this.keyChangeSelections = []; - this.meterChangeSelections = []; - this.selectedObjects = 0; - - // The scaling from ticks to pixels. - this.tickZoom = 0.15; - - // The tick grid which the cursor is snapped to. - this.tickSnap = 960 / 16; - - // This stores sections' representation objects, which tell things like - // the object positioning/layout, and where they can be interacted with the mouse. - this.viewRegions = []; - - // These control scrolling. - this.rowAtCenter = 8 * 5; - this.xAtLeft = 0; - - // These control cursor (the vertical blue bar) interaction. - this.CURSOR_ZONE_ALL = 0; - this.CURSOR_ZONE_NOTES = 1; - this.CURSOR_ZONE_CHORDS = 2; - - this.cursorTick = 0; - this.showCursor = true; - this.cursorZone = this.CURSOR_ZONE_ALL; - - // These control mouse interaction. - this.interactionEnabled = true; - this.mouseDown = false; - this.mouseDragAction = null; - this.mouseDragCurrent = { x: 0, y: 0 }; - this.mouseDragOrigin = { x: 0, y: 0 }; - this.mouseStretchPivotTick = 0; - this.mouseStretchOriginTick = 0; - this.mouseDraggedSectionKnob = -1; - - // These indicate which object the mouse is currently hovering over. - this.hoverNote = -1; - this.hoverChord = -1; - this.hoverKeyChange = -1; - this.hoverMeterChange = -1; - this.hoverSectionKnob = -1; - this.hoverStretchR = false; - this.hoverStretchL = false; - - // Layout constants. - this.MARGIN_LEFT = 16; - this.MARGIN_RIGHT = 16; - this.MARGIN_TOP = 4; - this.MARGIN_BOTTOM = 8; - this.SECTION_SEPARATION = 10; - this.HEADER_HEIGHT = 40; - this.HEADER_LINE_HEIGHT = 16; - this.NOTE_HEIGHT = 14; - this.NOTE_MARGIN_HOR = 0.5; - this.NOTE_MARGIN_VER = 0.5; - this.NOTE_STRETCH_MARGIN = 2; - this.CHORD_HEIGHT = 60; - this.CHORD_NOTE_SEPARATION = 10; - this.CHORD_ORNAMENT_HEIGHT = 5; - - // Finally, set the song data, and external dependencies. - this.theory = theory; - this.synth = synth; - this.setData(songData); -} - - -SongEditor.prototype.setData = function(songData) -{ - this.songData = songData; - this.refreshRepresentation(); -} - - -SongEditor.prototype.setInteractionEnabled = function(enable) -{ - this.interactionEnabled = enable; -} - - -SongEditor.prototype.addOnCursorChanged = function(func) -{ - this.cursorChangedCallbacks.push(func); -} - - -SongEditor.prototype.addOnSelectionChanged = function(func) -{ - this.selectionChangedCallbacks.push(func); -} - - -SongEditor.prototype.callOnCursorChanged = function() -{ - for (var i = 0; i < this.cursorChangedCallbacks.length; i++) - this.cursorChangedCallbacks[i](); -} - - -SongEditor.prototype.callOnSelectionChanged = function() -{ - for (var i = 0; i < this.selectionChangedCallbacks.length; i++) - this.selectionChangedCallbacks[i](); -} \ No newline at end of file diff --git a/src/editor_drawing.js b/src/editor_drawing.js deleted file mode 100644 index 173dd68..0000000 --- a/src/editor_drawing.js +++ /dev/null @@ -1,574 +0,0 @@ -SongEditor.prototype.refreshCanvas = function() -{ - this.canvasWidth = parseFloat(this.canvas.width); - this.canvasHeight = parseFloat(this.canvas.height); - - this.ctx.save(); - - - // Clear background. - this.ctx.fillStyle = "white"; - this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); - - - // Draw blocks. - var CURSOR_COLOR = "#0000ff"; - var SECTION_BORDER_COLOR = "#000000"; - var NOTE_LINE_COLOR = "#dddddd"; - var MEASURE_COLOR = "#dddddd"; - var MEASURE_COLOR_STRONG = "#888888"; - - this.ctx.save(); - this.ctx.beginPath(); - - for (var r = 0; r < this.viewRegions.length; r++) - { - var region = this.viewRegions[r]; - - // Draw section-stretched or regular region. - if (this.mouseDragAction == "stretch-section" && this.mouseDraggedSectionKnob == region.section) - { - var knobDraggedTick = this.getSectionKnobDraggedTick(region, this.mouseDragCurrent); - - if (region.sectionKnob && knobDraggedTick > region.tick + region.duration) - this.drawRegion(region, region.tick, knobDraggedTick - region.tick); - else - this.drawRegion(region, region.tick, region.duration, false); - } - else - this.drawRegion(region, region.tick, region.duration, false); - - this.ctx.save(); - this.ctx.rect( - region.x1, - region.y1 + this.HEADER_HEIGHT, - region.x2 - region.x1, - (region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - (region.y1 + this.HEADER_HEIGHT)); - this.ctx.clip(); - - // Draw notes. - for (var n = 0; n < region.notes.length; n++) - { - var noteIndex = region.notes[n]; - var note = this.songData.notes[noteIndex]; - - if (!this.noteSelections[noteIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") - this.drawNote(region, note.pitch, note.tick, note.duration, noteIndex == this.hoverNote, this.noteSelections[noteIndex]); - } - - // Draw dragged notes. - if (this.mouseDragAction != null && this.mouseDragAction != "scroll") - { - for (var n = 0; n < this.songData.notes.length; n++) - { - if (!this.noteSelections[n]) - continue; - - var note = this.songData.notes[n]; - var noteDragged = this.getNoteDragged(note, this.mouseDragCurrent); - this.drawNote(region, noteDragged.pitch, noteDragged.tick, noteDragged.duration, noteIndex == this.hoverNote, true); - } - } - - this.ctx.restore(); - - // Draw chords. - for (var n = 0; n < region.chords.length; n++) - { - var chordIndex = region.chords[n]; - var chord = this.songData.chords[chordIndex]; - - if (!this.chordSelections[chordIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") - this.drawChord(region, chord, chord.tick, chord.duration, chordIndex == this.hoverChord, this.chordSelections[chordIndex]); - } - - // Draw dragged chords. - if (this.mouseDragAction != null && this.mouseDragAction != "scroll") - { - for (var n = 0; n < this.songData.chords.length; n++) - { - if (!this.chordSelections[n]) - continue; - - var chord = this.songData.chords[n]; - var chordDragged = this.getChordDragged(chord, this.mouseDragCurrent); - this.drawChord(region, chord, chordDragged.tick, chordDragged.duration, chordIndex == this.hoverChord, true); - } - } - - // Draw key change. - for (var n = 0; n < region.keyChanges.length; n++) - { - var keyChangeIndex = region.keyChanges[n]; - var keyChange = this.songData.keyChanges[keyChangeIndex]; - - if (!this.keyChangeSelections[keyChangeIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") - this.drawKeyChange(region, keyChange, keyChange.tick, keyChangeIndex == this.hoverKeyChange, this.keyChangeSelections[keyChangeIndex]); - } - - // Draw dragged key changes. - if (this.mouseDragAction != null && this.mouseDragAction != "scroll") - { - for (var n = 0; n < this.songData.keyChanges.length; n++) - { - if (!this.keyChangeSelections[n]) - continue; - - var keyChange = this.songData.keyChanges[n]; - var keyChangeDragged = this.getKeyChangeDragged(keyChange, this.mouseDragCurrent); - - this.drawKeyChange(region, keyChange, keyChangeDragged.tick, keyChangeIndex == this.hoverKeyChange, true); - } - } - - // Draw meter change. - for (var n = 0; n < region.meterChanges.length; n++) - { - var meterChangeIndex = region.meterChanges[n]; - var meterChange = this.songData.meterChanges[meterChangeIndex]; - - if (!this.meterChangeSelections[meterChangeIndex] || this.mouseDragAction == null || this.mouseDragAction == "scroll") - this.drawMeterChange(region, meterChange, meterChange.tick, meterChangeIndex == this.hoverMeterChange, this.meterChangeSelections[meterChangeIndex]); - } - - // Draw dragged meter changes. - if (this.mouseDragAction != null && this.mouseDragAction != "scroll") - { - for (var n = 0; n < this.songData.meterChanges.length; n++) - { - if (!this.meterChangeSelections[n]) - continue; - - var meterChange = this.songData.meterChanges[n]; - var meterChangeDragged = this.getMeterChangeDragged(meterChange, this.mouseDragCurrent); - - this.drawMeterChange(region, meterChange, meterChangeDragged.tick, meterChangeIndex == this.hoverMeterChange, true); - } - } - - // Draw section knob and section deletion. - if (this.mouseDragAction == "stretch-section" && this.mouseDraggedSectionKnob == region.section) - { - var knobDraggedTick = this.getSectionKnobDraggedTick(region, this.mouseDragCurrent); - this.drawSectionKnob(region, knobDraggedTick, region.section == this.hoverSectionKnob, true, false); - - if (knobDraggedTick < region.tick + region.duration) - this.drawSectionDeletion(region, knobDraggedTick, region.tick + region.duration - knobDraggedTick); - } - else if (region.sectionKnob) - this.drawSectionKnob(region, region.tick + region.duration, region.section == this.hoverSectionKnob, false, true); - - // Draw cursor. - if (this.showCursor && this.cursorTick >= region.tick && this.cursorTick <= region.tick + region.duration) - { - this.ctx.strokeStyle = CURSOR_COLOR; - this.ctx.fillStyle = CURSOR_COLOR; - this.ctx.lineWidth = 2; - - var cursorX = region.x1 + (this.cursorTick - region.tick) * this.tickZoom; - var cursorY1 = region.y1 + this.HEADER_HEIGHT; - var cursorY2 = region.y2; - - if (this.cursorZone == this.CURSOR_ZONE_NOTES) - cursorY2 = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION; - - else if (this.cursorZone == this.CURSOR_ZONE_CHORDS) - cursorY1 = region.y2 - this.CHORD_HEIGHT; - - this.ctx.beginPath(); - this.ctx.moveTo(cursorX, cursorY1); - this.ctx.lineTo(cursorX, cursorY2); - this.ctx.stroke(); - - this.ctx.beginPath(); - this.ctx.moveTo(cursorX, cursorY1); - this.ctx.lineTo(cursorX - 6, cursorY1 - 6); - this.ctx.lineTo(cursorX + 6, cursorY1 - 6); - this.ctx.lineTo(cursorX, cursorY1); - this.ctx.fill(); - - this.ctx.beginPath(); - this.ctx.moveTo(cursorX, cursorY2); - this.ctx.lineTo(cursorX - 6, cursorY2 + 6); - this.ctx.lineTo(cursorX + 6, cursorY2 + 6); - this.ctx.lineTo(cursorX, cursorY2); - this.ctx.fill(); - } - } - - this.ctx.restore(); -} - - -SongEditor.prototype.drawNote = function(region, pitch, tick, duration, hovering, selected) -{ - // Check if the note is inside the region. - if (tick + duration <= region.tick || - tick >= region.tick + region.duration) - return; - - var deg = this.theory.getDegreeForPitch(pitch, region.key.scale, region.key.tonicPitch); - var row = this.theory.getRowForPitch(pitch, region.key.scale, region.key.tonicPitch); - - var clippedStartTick = Math.max(tick, region.tick); - var clippedEndTick = Math.min(tick + duration, region.tick + region.duration); - var clippedDuration = clippedEndTick - clippedStartTick; - - var pos = this.getNotePosition(region, row, clippedStartTick, clippedDuration); - - this.drawDegreeColoredRectangle(deg, pos.x1 + this.NOTE_MARGIN_HOR, pos.y1, pos.x2 - this.NOTE_MARGIN_HOR, pos.y2); - - // Draw highlights. - this.ctx.save(); - - if (selected) - { - this.ctx.globalAlpha = 0.3; - this.ctx.fillStyle = "#ffffff"; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - this.ctx.fillRect(pos.x1, pos.y1 + 3, pos.x2 - pos.x1, pos.y2 - pos.y1 - 6); - } - else if (hovering) - { - this.ctx.globalAlpha = 0.3; - this.ctx.fillStyle = "#ffffff"; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - } - - this.ctx.restore(); -} - - -SongEditor.prototype.drawChord = function(region, chord, tick, duration, hovering, selected) -{ - // Check if the chord is inside the region. - if (tick + duration <= region.tick || - tick >= region.tick + region.duration) - return; - - var clippedStartTick = Math.max(tick, region.tick); - var clippedEndTick = Math.min(tick + duration, region.tick + region.duration); - var clippedDuration = clippedEndTick - clippedStartTick; - - var deg = this.theory.getDegreeForPitch(chord.rootPitch, region.key.scale, region.key.tonicPitch); - var pos = this.getChordPosition(region, clippedStartTick, clippedDuration); - - this.drawDegreeColoredRectangle(deg, pos.x1 + this.NOTE_MARGIN_HOR, pos.y1, pos.x2 - this.NOTE_MARGIN_HOR, pos.y1 + this.CHORD_ORNAMENT_HEIGHT); - this.drawDegreeColoredRectangle(deg, pos.x1 + this.NOTE_MARGIN_HOR, pos.y2 - this.CHORD_ORNAMENT_HEIGHT, pos.x2 - this.NOTE_MARGIN_HOR, pos.y2); - - // Draw roman symbol. - var numeral = this.theory.getRomanNumeralForPitch(chord.rootPitch, region.key.scale, region.key.tonicPitch); - var romanText = (chord.chord.uppercase ? numeral : numeral.toLowerCase()); - - this.ctx.fillStyle = "#000000"; - this.ctx.textAlign = "center"; - this.ctx.textBaseline = "middle"; - - this.ctx.font = "20px Tahoma"; - var supTextWidth = this.ctx.measureText(chord.chord.symbolSup).width; - var subTextWidth = this.ctx.measureText(chord.chord.symbolSub).width; - - this.ctx.font = "30px Tahoma"; - var mainTextWidth = this.ctx.measureText(romanText).width; - var totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; - - var maxTextWidth = pos.x2 - pos.x1 - 2; - if (totalTextWidth > maxTextWidth) - { - var proportion = totalTextWidth / maxTextWidth; - supTextWidth /= proportion; - subTextWidth /= proportion; - mainTextWidth /= proportion; - totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; - } - - this.ctx.fillText(romanText, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth / 2, (pos.y1 + pos.y2) / 2, maxTextWidth - supTextWidth - subTextWidth); - - this.ctx.font = "20px Tahoma"; - this.ctx.fillText(chord.chord.symbolSup, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, (pos.y1 + pos.y2) / 2 - 10, maxTextWidth - mainTextWidth - subTextWidth); - this.ctx.fillText(chord.chord.symbolSub, (pos.x1 + pos.x2) / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth + subTextWidth / 2, (pos.y1 + pos.y2) / 2 + 10, maxTextWidth - mainTextWidth - supTextWidth); - - // Draw highlights. - this.ctx.save(); - - if (selected) - { - this.ctx.globalAlpha = 0.3; - this.ctx.fillStyle = "#ffffff"; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - this.ctx.fillRect(pos.x1, pos.y1 + 3, pos.x2 - pos.x1, pos.y2 - pos.y1 - 6); - } - else if (hovering) - { - this.ctx.globalAlpha = 0.3; - this.ctx.fillStyle = "#ffffff"; - this.ctx.fillRect(pos.x1, pos.y1, pos.x2 - pos.x1, pos.y2 - pos.y1); - } - - this.ctx.restore(); -} - - -SongEditor.prototype.drawKeyChange = function(region, keyChange, tick, hovering, selected) -{ - // Check if the key change is inside the region. - if (tick < region.tick || - tick >= region.tick + region.duration) - return; - - var COLOR = "#aaaaaa"; - - var x = region.x1 + (tick - region.tick) * this.tickZoom; - - this.ctx.strokeStyle = COLOR; - this.ctx.lineWidth = 2; - - this.ctx.beginPath(); - this.ctx.moveTo(x, region.y1); - this.ctx.lineTo(x, region.y2); - this.ctx.stroke(); - - if (selected) - { - this.ctx.save(); - this.ctx.globalAlpha = 0.6; - this.ctx.fillStyle = COLOR; - this.ctx.fillRect(x, region.y1, region.x2 - x, this.HEADER_LINE_HEIGHT); - this.ctx.restore(); - } - else if (hovering) - { - this.ctx.save(); - this.ctx.globalAlpha = 0.2; - this.ctx.fillStyle = COLOR; - this.ctx.fillRect(x, region.y1, region.x2 - x, this.HEADER_LINE_HEIGHT); - this.ctx.restore(); - } - - this.ctx.font = "14px Tahoma"; - this.ctx.textAlign = "left"; - this.ctx.textBaseline = "middle"; - this.ctx.fillStyle = COLOR; - this.ctx.fillText( - this.theory.getNameForPitch(keyChange.tonicPitch, keyChange.scale, keyChange.tonicPitch) + " " + keyChange.scale.name, - x + 8, - region.y1 + 8, - region.x2 - x - 16); -} - - -SongEditor.prototype.drawMeterChange = function(region, meterChange, tick, hovering, selected) -{ - // Check if the meter change is inside the region. - if (tick < region.tick || - tick >= region.tick + region.duration) - return; - - var COLOR = "#88aaaa"; - - var x = region.x1 + (tick - region.tick) * this.tickZoom; - - this.ctx.strokeStyle = COLOR; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - this.ctx.moveTo(x, region.y1 + this.HEADER_LINE_HEIGHT); - this.ctx.lineTo(x, region.y2); - this.ctx.stroke(); - - if (selected) - { - this.ctx.save(); - this.ctx.globalAlpha = 0.6; - this.ctx.fillStyle = COLOR; - this.ctx.fillRect(x, region.y1 + this.HEADER_LINE_HEIGHT, region.x2 - x, this.HEADER_LINE_HEIGHT); - this.ctx.restore(); - } - else if (hovering) - { - this.ctx.save(); - this.ctx.globalAlpha = 0.2; - this.ctx.fillStyle = COLOR; - this.ctx.fillRect(x, region.y1 + this.HEADER_LINE_HEIGHT, region.x2 - x, this.HEADER_LINE_HEIGHT); - this.ctx.restore(); - } - - this.ctx.font = "14px Tahoma"; - this.ctx.textAlign = "left"; - this.ctx.textBaseline = "middle"; - this.ctx.fillStyle = COLOR; - this.ctx.fillText( - "" + meterChange.numerator + " / " + meterChange.denominator, - x + 8, - region.y1 + 24, - region.x2 - x - 16); -} - - -SongEditor.prototype.drawSectionKnob = function(region, tick, hovering, selected, clipOutside) -{ - // Check if the knob is inside the region. - if (clipOutside && - (tick < region.tick || - tick > region.tick + region.duration)) - return; - - var COLOR = "#000000"; - var COLOR_HOVER = "#88ddff"; - var COLOR_SELECTED = "#4488ff"; - - var x = region.x1 + (tick - region.tick) * this.tickZoom; - - this.ctx.strokeStyle = (selected ? COLOR_SELECTED : hovering ? COLOR_HOVER : COLOR); - this.ctx.fillStyle = (selected ? COLOR_SELECTED : hovering ? COLOR_HOVER : COLOR); - this.ctx.lineWidth = 2; - - this.ctx.beginPath(); - this.ctx.moveTo(x, region.y1 + this.HEADER_HEIGHT - 12); - this.ctx.lineTo(x, region.y2); - this.ctx.stroke(); - - this.ctx.beginPath(); - this.ctx.arc(x, region.y1 + this.HEADER_HEIGHT - 12, 6, 0, Math.PI * 2); - this.ctx.fill(); -} - - -SongEditor.prototype.drawSectionDeletion = function(region, tick, duration) -{ - // Check if the knob is inside the region. - if (tick + duration < region.tick || - tick >= region.tick + region.duration) - return; - - var COLOR = "#ffffff"; - - var clippedStart = Math.max(region.tick, tick); - var clippedEnd = Math.min(region.tick + region.duration, tick + duration); - - var x1 = region.x1 + (clippedStart - region.tick) * this.tickZoom; - var x2 = region.x1 + (clippedEnd - region.tick) * this.tickZoom; - - this.ctx.save(); - this.ctx.fillStyle = COLOR; - this.ctx.globalAlpha = 0.8; - - this.ctx.fillRect(x1 + 1, region.y1 + this.HEADER_HEIGHT - 1, x2 - x1, region.y2 - region.y1 - this.HEADER_HEIGHT + 2); - - this.ctx.restore(); -} - - -SongEditor.prototype.drawRegion = function(region, tick, duration, asSectionStretch) -{ - var SECTION_BORDER_COLOR = "#000000"; - var NOTE_LINE_COLOR = "#dddddd"; - var MEASURE_COLOR = "#dddddd"; - var MEASURE_COLOR_STRONG = "#888888"; - - var x1 = region.x1 + (tick - region.tick) * this.tickZoom; - var x2 = region.x1 + (tick + duration - region.tick) * this.tickZoom; - - // Draw region boundaries. - this.ctx.strokeStyle = SECTION_BORDER_COLOR; - this.ctx.lineWidth = 2; - this.ctx.beginPath(); - - this.ctx.strokeRect( - x1, - region.y1 + this.HEADER_HEIGHT, - x2 - x1, - (region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - (region.y1 + this.HEADER_HEIGHT)); - - this.ctx.strokeRect( - x1, - region.y2 - this.CHORD_HEIGHT, - x2 - x1, - this.CHORD_HEIGHT); - - this.ctx.stroke(); - - // Draw note lines. - this.ctx.strokeStyle = NOTE_LINE_COLOR; - this.ctx.beginPath(); - for (var n = 1; n < region.highestNoteRow - region.lowestNoteRow; n++) - { - var y = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - n * this.NOTE_HEIGHT; - this.ctx.moveTo(x1 + 1, y); - this.ctx.lineTo(x2 - 1, y); - } - this.ctx.stroke(); - - // Draw beat lines. - var beatCount = 0; - for (var n = region.meter.tick - tick; n < duration; n += this.songData.ticksPerWholeNote / region.meter.denominator) - { - if (n > 0) - { - this.ctx.strokeStyle = (beatCount == 0 ? MEASURE_COLOR_STRONG : MEASURE_COLOR); - this.ctx.beginPath(); - this.ctx.moveTo( - x1 + n * this.tickZoom, - region.y1 + this.HEADER_HEIGHT + 1); - this.ctx.lineTo( - x1 + n * this.tickZoom, - region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - 1); - this.ctx.moveTo( - x1 + n * this.tickZoom, - region.y2 - this.CHORD_HEIGHT + 1); - this.ctx.lineTo( - x1 + n * this.tickZoom, - region.y2 - 1); - this.ctx.stroke(); - } - - beatCount = (beatCount + 1) % region.meter.numerator; - } -} - - -SongEditor.prototype.drawDegreeColoredRectangle = function(deg, x1, y1, x2, y2) -{ - var col = this.theory.getColorForDegree(deg - (deg % 1)); - - this.ctx.save(); - this.ctx.beginPath(); - this.ctx.rect(x1, y1, x2 - x1, y2 - y1); - this.ctx.clip(); - - this.ctx.fillStyle = col; - this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); - - // Draw stripes for fractional scale degrees. - if ((deg % 1) != 0) - { - this.ctx.strokeStyle = this.theory.getColorForDegree((deg - (deg % 1) + 1) % 7); - this.ctx.lineWidth = 5; - for (var i = 0; i < x2 - x1 + 30; i += 15) - { - this.ctx.beginPath(); - this.ctx.moveTo(x1 + i, y1 - 5); - this.ctx.lineTo(x1 + i - 20, y1 - 5 + 20); - this.ctx.stroke(); - } - } - - this.ctx.restore(); -} - - -SongEditor.prototype.getYForRow = function(block, row) -{ - var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; - return block.y2 - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN - noteAreaHeight / 2 + (this.rowAtCenter - row) * this.NOTE_HEIGHT; -} - - -SongEditor.prototype.getFirstAndLastVisibleRowsForBlock = function(block) -{ - var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; - - return { - first: Math.max(Math.floor(this.rowAtCenter - noteAreaHeight / 2 / this.NOTE_HEIGHT), 7 * 3), - last: Math.min(Math.ceil(this.rowAtCenter + noteAreaHeight / 2 / this.NOTE_HEIGHT), 7 * 8 - 1) - }; -} \ No newline at end of file diff --git a/src/editor_event_handling.js b/src/editor_event_handling.js deleted file mode 100644 index bbc20ab..0000000 --- a/src/editor_event_handling.js +++ /dev/null @@ -1,614 +0,0 @@ -function transformMousePosition(elem, ev) -{ - var rect = elem.getBoundingClientRect(); - return { x: ev.clientX - rect.left, y: ev.clientY - rect.top }; -} - - -SongEditor.prototype.handleMouseMove = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - if (!this.interactionEnabled) - return; - - this.canvas.style.cursor = "default"; - this.mouseDragCurrent = mousePos; - this.clearHover(); - - // Check what's under the mouse, if it's not down. - if (!this.mouseDown) - { - var regionIndex = this.getRegionAtPosition(mousePos); - if (regionIndex != -1) - { - var region = this.viewRegions[regionIndex]; - if (isPointInside(mousePos, region.interactBBox)) - { - // Check for the section knob. - if (region.sectionKnob) - { - var knobPos = this.getSectionKnobPosition(region); - if (isPointInside(mousePos, knobPos)) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverSectionKnob = region.section; - } - } - - // Check for notes. - if (this.hoverSectionKnob < 0) - { - for (var n = 0; n < region.notes.length; n++) - { - var note = this.songData.notes[region.notes[n]]; - var noteRow = this.theory.getRowForPitch(note.pitch, region.key.scale, region.key.tonicPitch); - var notePos = this.getNotePosition(region, noteRow, note.tick, note.duration); - - if (isPointInside(mousePos, notePos) && isPointInside(mousePos, region)) - { - this.hoverNote = region.notes[n]; - - if (mousePos.x <= notePos.x1 + this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchL = true; - } - else if (mousePos.x >= notePos.x2 - this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchR = true; - } - else - this.canvas.style.cursor = "pointer"; - - break; - } - } - } - - // Check for chords. - if (this.hoverSectionKnob < 0 && this.hoverNote < 0) - { - for (var n = 0; n < region.chords.length; n++) - { - var chord = this.songData.chords[region.chords[n]]; - var chordPos = this.getChordPosition(region, chord.tick, chord.duration); - - if (isPointInside(mousePos, chordPos) && isPointInside(mousePos, region)) - { - this.hoverChord = region.chords[n]; - - if (mousePos.x <= chordPos.x1 + this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchL = true; - } - else if (mousePos.x >= chordPos.x2 - this.NOTE_STRETCH_MARGIN) - { - this.canvas.style.cursor = "ew-resize"; - this.hoverStretchR = true; - } - else - this.canvas.style.cursor = "pointer"; - - break; - } - } - } - } - - // Check for key changes. - if (this.hoverSectionKnob < 0 && this.hoverNote < 0 && this.hoverChord < 0) - { - for (var n = 0; n < region.keyChanges.length; n++) - { - var keyChange = this.songData.keyChanges[region.keyChanges[n]]; - var keyChangePos = this.getKeyChangePosition(region, keyChange.tick); - - if (isPointInside(mousePos, keyChangePos)) - { - this.hoverKeyChange = region.keyChanges[n]; - this.canvas.style.cursor = "pointer"; - break; - } - } - } - - // Check for meter changes. - if (this.hoverSectionKnob < 0 && this.hoverNote < 0 && this.hoverChord < 0 && this.hoverKeyChange < 0) - { - for (var n = 0; n < region.meterChanges.length; n++) - { - var meterChange = this.songData.meterChanges[region.meterChanges[n]]; - var meterChangePos = this.getMeterChangePosition(region, meterChange.tick); - - if (isPointInside(mousePos, meterChangePos)) - { - this.hoverMeterChange = region.meterChanges[n]; - this.canvas.style.cursor = "pointer"; - break; - } - } - } - } - - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "move") - { - this.canvas.style.cursor = "move"; - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "stretch") - { - this.canvas.style.cursor = "ew-resize"; - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "stretch-section") - { - this.canvas.style.cursor = "ew-resize"; - this.refreshCanvas(); - } - - else if (this.mouseDragAction == "scroll") - { - /*var rowOffset = (mousePos.y - this.mouseDragOrigin.y) / this.NOTE_HEIGHT; - this.rowAtCenter += rowOffset; - var rowLimits = this.getFirstAndLastScrollableRows(); - this.rowAtCenter = Math.min(rowLimits.first, Math.max(rowLimits.last, this.rowAtCenter)); - this.mouseDragOrigin.y = mousePos.y; - - var xOffset = (this.mouseDragOrigin.x - mousePos.x); - this.xAtLeft += xOffset; - this.xAtLeft = Math.max(0, Math.min(this.viewBlocks[this.viewBlocks.length - 1].x2 + this.canvasWidth / 2, this.xAtLeft)); - this.mouseDragOrigin.x = mousePos.x; - - this.refreshRepresentation();*/ - this.refreshCanvas(); - } -} - - -SongEditor.prototype.handleMouseDown = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - if (!this.interactionEnabled) - return; - - if (!ev.ctrlKey) - this.unselectAll(); - - this.mouseDragAction = null; - this.mouseDragOrigin = mousePos; - - this.cursorTick = this.getTickAtPosition(mousePos); - this.cursorZone = this.getZoneAtPosition(mousePos); - this.showCursor = true; - - // Start a dragging operation. - if (this.hoverSectionKnob >= 0) - { - this.unselectAll(); - this.showCursor = false; - this.mouseDragAction = "stretch-section"; - this.mouseDraggedSectionKnob = this.hoverSectionKnob; - } - else if (this.hoverNote >= 0) - { - this.showCursor = false; - if (!this.noteSelections[this.hoverNote]) - this.selectedObjects++; - - this.noteSelections[this.hoverNote] = true; - this.cursorTick = this.songData.notes[this.hoverNote].tick; - this.theory.playNoteSample(this.synth, this.songData.notes[this.hoverNote].pitch); - - if (this.hoverStretchR) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick + this.songData.notes[this.hoverNote].duration; - this.mouseStretchPivotTick = this.getEarliestSelectedTick(); - } - else if (this.hoverStretchL) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.notes[this.hoverNote].tick; - this.mouseStretchPivotTick = this.getLatestSelectedTick(); - } - else - this.mouseDragAction = "move"; - } - else if (this.hoverChord >= 0) - { - this.showCursor = false; - if (!this.chordSelections[this.hoverChord]) - this.selectedObjects++; - - this.chordSelections[this.hoverChord] = true; - this.cursorTick = this.songData.chords[this.hoverChord].tick; - this.theory.playChordSample(this.synth, this.songData.chords[this.hoverChord].chord, this.songData.chords[this.hoverChord].rootPitch); - - if (this.hoverStretchR) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick + this.songData.chords[this.hoverChord].duration; - this.mouseStretchPivotTick = this.getEarliestSelectedTick(); - } - else if (this.hoverStretchL) - { - this.mouseDragAction = "stretch"; - this.mouseStretchOriginTick = this.songData.chords[this.hoverChord].tick; - this.mouseStretchPivotTick = this.getLatestSelectedTick(); - } - else - this.mouseDragAction = "move"; - } - else if (this.hoverKeyChange >= 0) - { - if (!this.keyChangeSelections[this.hoverKeyChange]) - this.selectedObjects++; - - this.showCursor = false; - this.keyChangeSelections[this.hoverKeyChange] = true; - this.mouseDragAction = "move"; - } - else if (this.hoverMeterChange >= 0) - { - if (!this.meterChangeSelections[this.hoverMeterChange]) - this.selectedObjects++; - - this.showCursor = false; - this.meterChangeSelections[this.hoverMeterChange] = true; - this.mouseDragAction = "move"; - } - else - { - this.mouseDragAction = "scroll"; - this.clearHover(); - } - - this.mouseDown = true; - this.callOnSelectionChanged(); - this.refreshCanvas(); - this.callOnCursorChanged(); -} - - -SongEditor.prototype.handleMouseUp = function(ev) -{ - ev.preventDefault(); - var mousePos = transformMousePosition(this.canvas, ev); - - if (!this.interactionEnabled) - return; - - // Apply dragged section knob. - if (this.mouseDown && this.mouseDragAction == "stretch-section") - { - var regionStretched = null; - for (var r = 0; r < this.viewRegions.length; r++) - { - var region = this.viewRegions[r]; - if (region.section == this.mouseDraggedSectionKnob && region.sectionKnob) - { - regionStretched = region; - break; - } - } - - var draggedKnobTick = this.getSectionKnobDraggedTick(regionStretched, mousePos); - - if (draggedKnobTick > region.tick + region.duration) - { - this.songData.insertWhitespace(region.tick + region.duration, draggedKnobTick - region.tick - region.duration); - this.refreshRepresentation(); - } - else if (draggedKnobTick < region.tick + region.duration) - { - this.songData.remove(draggedKnobTick, region.tick + region.duration - draggedKnobTick); - this.refreshRepresentation(); - } - } - - // Apply dragged modifications. - else if (this.mouseDown && this.mouseDragAction != null && this.mouseDragAction != "scroll") - { - // Store modified objects in a local array and - // remove them from the song data. - var selectedNotes = []; - for (var n = this.noteSelections.length - 1; n >= 0; n--) - { - if (this.noteSelections[n]) - { - selectedNotes.push(this.songData.notes[n]); - this.songData.notes.splice(n, 1); - } - } - - var selectedChords = []; - for (var n = this.chordSelections.length - 1; n >= 0; n--) - { - if (this.chordSelections[n]) - { - selectedChords.push(this.songData.chords[n]); - this.songData.chords.splice(n, 1); - } - } - - var selectedKeyChanges = []; - for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) - { - if (this.keyChangeSelections[n]) - { - selectedKeyChanges.push(this.songData.keyChanges[n]); - this.songData.keyChanges.splice(n, 1); - } - } - - var selectedMeterChanges = []; - for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) - { - if (this.meterChangeSelections[n]) - { - selectedMeterChanges.push(this.songData.meterChanges[n]); - this.songData.meterChanges.splice(n, 1); - } - } - - // Apply the modification and reinsert objects into the song data. - var newNotes = []; - for (var n = 0; n < selectedNotes.length; n++) - { - var draggedNote = this.getNoteDragged(selectedNotes[n], mousePos); - var newNote = new SongDataNote(draggedNote.tick, draggedNote.duration, draggedNote.pitch); - newNotes.push(newNote); - this.songData.addNote(newNote); - } - - var newChords = []; - for (var n = 0; n < selectedChords.length; n++) - { - var draggedChord = this.getChordDragged(selectedChords[n], mousePos); - var newChord = new SongDataChord(draggedChord.tick, draggedChord.duration, selectedChords[n].chord, selectedChords[n].rootPitch); - newChords.push(newChord); - this.songData.addChord(newChord); - } - - var newKeyChanges = []; - for (var n = 0; n < selectedKeyChanges.length; n++) - { - var draggedKeyChange = this.getKeyChangeDragged(selectedKeyChanges[n], mousePos); - var newKeyChange = new SongDataKeyChange(draggedKeyChange.tick, selectedKeyChanges[n].scale, selectedKeyChanges[n].tonicPitch); - newKeyChanges.push(newKeyChange); - this.songData.addKeyChange(newKeyChange); - } - - var newMeterChanges = []; - for (var n = 0; n < selectedMeterChanges.length; n++) - { - var draggedMeterChange = this.getMeterChangeDragged(selectedMeterChanges[n], mousePos); - var newMeterChange = new SongDataMeterChange(draggedMeterChange.tick, selectedMeterChanges[n].numerator, selectedMeterChanges[n].denominator); - newMeterChanges.push(newMeterChange); - this.songData.addMeterChange(newMeterChange); - } - - this.clearHover(); - this.refreshRepresentation(); - - // Find which objects were selected before, and select them again. - for (var n = 0; n < this.songData.notes.length; n++) - { - for (var m = 0; m < newNotes.length; m++) - { - if (this.songData.notes[n] == newNotes[m]) - { - this.selectedObjects++; - this.noteSelections[n] = true; - } - } - } - - for (var n = 0; n < this.songData.chords.length; n++) - { - for (var m = 0; m < newChords.length; m++) - { - if (this.songData.chords[n] == newChords[m]) - { - this.selectedObjects++; - this.chordSelections[n] = true; - } - } - } - - for (var n = 0; n < this.songData.keyChanges.length; n++) - { - for (var m = 0; m < newKeyChanges.length; m++) - { - if (this.songData.keyChanges[n] == newKeyChanges[m]) - { - this.selectedObjects++; - this.keyChangeSelections[n] = true; - } - } - } - - for (var n = 0; n < this.songData.meterChanges.length; n++) - { - for (var m = 0; m < newMeterChanges.length; m++) - { - if (this.songData.meterChanges[n] == newMeterChanges[m]) - { - this.selectedObjects++; - this.meterChangeSelections[n] = true; - } - } - } - } - - this.mouseDown = false; - this.mouseDragAction = null; - this.refreshCanvas(); -} - - -SongEditor.prototype.handleKeyDown = function(ev) -{ - if (!this.interactionEnabled) - return; - - var keyCode = (ev.key || ev.which || ev.keyCode); - - if (keyCode == 46 || (keyCode == 8 && this.selectedObjects > 0)) // Delete/Backspace - { - ev.preventDefault(); - - // Remove selected objects from the song data. - var selectedNotes = []; - for (var n = this.noteSelections.length - 1; n >= 0; n--) - { - if (this.noteSelections[n]) - { - selectedNotes.push(this.songData.notes[n]); - this.songData.notes.splice(n, 1); - } - } - - var selectedChords = []; - for (var n = this.chordSelections.length - 1; n >= 0; n--) - { - if (this.chordSelections[n]) - { - selectedChords.push(this.songData.chords[n]); - this.songData.chords.splice(n, 1); - } - } - - var selectedKeyChanges = []; - for (var n = this.keyChangeSelections.length - 1; n >= 0; n--) - { - if (this.keyChangeSelections[n]) - { - selectedKeyChanges.push(this.songData.keyChanges[n]); - this.songData.keyChanges.splice(n, 1); - } - } - - var selectedMeterChanges = []; - for (var n = this.meterChangeSelections.length - 1; n >= 0; n--) - { - if (this.meterChangeSelections[n]) - { - selectedMeterChanges.push(this.songData.meterChanges[n]); - this.songData.meterChanges.splice(n, 1); - } - } - - this.showCursor = true; - this.clearHover(); - this.refreshRepresentation(); - this.refreshCanvas(); - this.callOnSelectionChanged(); - } - - else if (keyCode == 8 && this.selectedObjects == 0) // Backspace - { - ev.preventDefault(); - - var lastTickToConsider = 0; - var considerNotes = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_NOTES); - var considerChords = (this.cursorZone == this.CURSOR_ZONE_ALL || this.cursorZone == this.CURSOR_ZONE_CHORDS); - - if (considerNotes) - { - for (var i = 0; i < this.songData.notes.length; i++) - { - var note = this.songData.notes[i]; - if (note.tick + note.duration > lastTickToConsider) - lastTickToConsider = Math.min(note.tick + note.duration, this.cursorTick); - } - } - - if (considerChords) - { - for (var i = 0; i < this.songData.chords.length; i++) - { - var chord = this.songData.chords[i]; - if (chord.tick + chord.duration > lastTickToConsider) - lastTickToConsider = Math.min(chord.tick + chord.duration, this.cursorTick); - } - } - - if (lastTickToConsider != this.cursorTick) - { - this.cursorTick = Math.max(0, lastTickToConsider); - - this.clearHover(); - this.refreshCanvas(); - this.callOnCursorChanged(); - return; - } - - var deleteBeginTick = 0; - - if (considerNotes) - { - for (var i = 0; i < this.songData.notes.length; i++) - { - var note = this.songData.notes[i]; - if (note.tick + note.duration > lastTickToConsider) - continue; - - if (note.tick + note.duration == lastTickToConsider && - note.tick > deleteBeginTick) - { - deleteBeginTick = note.tick; - } - else if (note.tick + note.duration != lastTickToConsider && - note.tick + note.duration > deleteBeginTick) - { - deleteBeginTick = note.tick + note.duration; - } - } - } - - if (considerChords) - { - for (var i = 0; i < this.songData.chords.length; i++) - { - var chord = this.songData.chords[i]; - if (chord.tick + chord.duration > lastTickToConsider) - continue; - - if (chord.tick + chord.duration == lastTickToConsider && - chord.tick > deleteBeginTick) - { - deleteBeginTick = chord.tick; - } - else if (chord.tick + chord.duration != lastTickToConsider && - chord.tick + chord.duration > deleteBeginTick) - { - deleteBeginTick = chord.tick + chord.duration; - } - } - } - - if (considerNotes) - this.songData.removeNotesByTickRange(deleteBeginTick, lastTickToConsider, null); - - if (considerChords) - this.songData.removeChordsByTickRange(deleteBeginTick, lastTickToConsider); - - this.cursorTick = Math.max(0, deleteBeginTick); - this.clearHover(); - this.refreshRepresentation(); - this.refreshCanvas(); - this.callOnCursorChanged(); - } -} \ No newline at end of file diff --git a/src/editor_interaction.js b/src/editor_interaction.js deleted file mode 100644 index 98b5380..0000000 --- a/src/editor_interaction.js +++ /dev/null @@ -1,383 +0,0 @@ -SongEditor.prototype.unselectAll = function() -{ - for (var i = 0; i < this.noteSelections.length; i++) - this.noteSelections[i] = false; - - for (var i = 0; i < this.chordSelections.length; i++) - this.chordSelections[i] = false; - - for (var i = 0; i < this.keyChangeSelections.length; i++) - this.keyChangeSelections[i] = false; - - for (var i = 0; i < this.meterChangeSelections.length; i++) - this.meterChangeSelections[i] = false; - - this.selectedObjects = 0; - this.callOnSelectionChanged(); -} - - -SongEditor.prototype.selectKeyChange = function(keyChange) -{ - for (var n = 0; n < this.songData.keyChanges.length; n++) - { - if (this.songData.keyChanges[n] == keyChange) - { - this.selectedObjects++; - this.keyChangeSelections[n] = true; - } - } -} - - -SongEditor.prototype.selectMeterChange = function(meterChange) -{ - for (var n = 0; n < this.songData.meterChanges.length; n++) - { - if (this.songData.meterChanges[n] == meterChange) - { - this.selectedObjects++; - this.meterChangeSelections[n] = true; - } - } -} - - -SongEditor.prototype.getUniqueKeyChangeSelected = function() -{ - if (this.selectedObjects != 1) - return null; - - for (var i = 0; i < this.keyChangeSelections.length; i++) - { - if (this.keyChangeSelections[i]) - return this.songData.keyChanges[i]; - } - - return null; -} - - -SongEditor.prototype.getUniqueMeterChangeSelected = function() -{ - if (this.selectedObjects != 1) - return null; - - for (var i = 0; i < this.meterChangeSelections.length; i++) - { - if (this.meterChangeSelections[i]) - return this.songData.meterChanges[i]; - } - - return null; -} - - -SongEditor.prototype.clearHover = function() -{ - this.hoverNote = -1; - this.hoverChord = -1; - this.hoverKeyChange = -1; - this.hoverMeterChange = -1; - this.hoverStretchR = false; - this.hoverStretchL = false; - this.hoverSectionKnob = -1; -} - - -function isPointInside(p, rect) -{ - return ( - p.x >= rect.x1 && p.x <= rect.x2 && - p.y >= rect.y1 && p.y <= rect.y2); -} - - -SongEditor.prototype.getPositionForTick = function(tick) -{ - for (var b = 0; b < this.viewBlocks.length; b++) - { - var block = this.viewBlocks[b]; - - if (tick >= block.tick && tick < block.tick + block.duration) - { - return block.x1 + (tick - block.tick) * this.tickZoom; - } - } - - return 0; -} - - -SongEditor.prototype.getRegionAtPosition = function(pos) -{ - for (var r = 0; r < this.viewRegions.length; r++) - { - var region = this.viewRegions[r]; - - if (isPointInside(pos, region.interactBBox)) - return r; - } - - return -1; -} - - -SongEditor.prototype.getTickAtPosition = function(pos) -{ - var regionIndex = this.getRegionAtPosition(pos); - if (regionIndex < 0) - return -1; - - var region = this.viewRegions[regionIndex]; - return Math.round((region.tick + Math.min(region.duration, Math.ceil((pos.x - region.x1) / this.tickZoom))) / this.tickSnap) * this.tickSnap; -} - - -SongEditor.prototype.getTickAtPositionAtRegion = function(region, pos) -{ - return Math.round((region.tick + Math.ceil((pos.x - region.x1) / this.tickZoom)) / this.tickSnap) * this.tickSnap; -} - - -SongEditor.prototype.getZoneAtPosition = function(pos) -{ - var regionIndex = this.getRegionAtPosition(pos); - if (regionIndex < 0) - return this.CURSOR_ZONE_ALL; - - var region = this.viewRegions[regionIndex]; - if (pos.y >= region.y1 + this.HEADER_HEIGHT && - pos.y <= region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION) - return this.CURSOR_ZONE_NOTES; - - else if (pos.y >= region.y2 - this.CHORD_HEIGHT && - pos.y <= region.y2) - return this.CURSOR_ZONE_CHORDS; - - else - return this.CURSOR_ZONE_ALL; -} - - -SongEditor.prototype.getRegionAtTick = function(tick) -{ - for (var r = 0; r < this.viewRegions.length; r++) - { - var region = this.viewRegions[r]; - - if (tick >= region.tick && tick < region.tick + region.duration) - return r; - } - - return -1; -} - - -SongEditor.prototype.getKeyAtTick = function(tick) -{ - var index = this.getRegionAtTick(tick); - if (index >= 0) - return this.viewRegions[index].key; - - return null; -} - - -SongEditor.prototype.getMeterAtTick = function(tick) -{ - var index = this.getRegionAtTick(tick); - if (index >= 0) - return this.viewRegions[index].meter; - - return null; -} - - -SongEditor.prototype.getEarliestSelectedTick = function() -{ - // FIXME: Take key/meter changes into consideration. - // TODO: Use binary search. - var earliest = 1000000; - for (var n = 0; n < this.noteSelections.length; n++) - { - if (this.noteSelections[n]) - { - earliest = this.songData.notes[n].tick; - break; - } - } - - for (var n = 0; n < this.chordSelections.length; n++) - { - if (this.chordSelections[n]) - { - if (this.songData.chords[n].tick < earliest) - return this.songData.chords[n].tick; - else if (this.songData.chords[n].tick >= earliest) - break; - } - } - - return earliest; -} - - -SongEditor.prototype.getLatestSelectedTick = function() -{ - // TODO: Take key/meter changes into consideration. - // TODO: Use binary search. - var latest = 0; - for (var n = this.noteSelections.length - 1; n >= 0; n--) - { - if (this.noteSelections[n]) - { - var tick = this.songData.notes[n].tick + this.songData.notes[n].duration; - if (tick < latest) - latest = tick; - } - } - - for (var n = this.chordSelections.length - 1; n >= 0; n--) - { - if (this.chordSelections[n]) - { - var tick = this.songData.chords[n].tick + this.songData.chords[n].duration; - if (tick < latest) - latest = tick; - } - } - - return tick; -} - - -SongEditor.prototype.getFirstAndLastScrollableRows = function() -{ - var noteAreaHeight = this.canvasHeight - this.MARGIN_TOP - this.HEADER_MARGIN - this.CHORD_HEIGHT - this.CHORDNOTE_MARGIN; - - // FIXME: Hardcoded values. - return { - first: Math.floor(7 * 9 - noteAreaHeight / 2 / this.NOTE_HEIGHT), - last: Math.ceil(7 * 2 + noteAreaHeight / 2 / this.NOTE_HEIGHT) - }; -} - - -SongEditor.prototype.getNoteDragged = function(note, dragPosition) -{ - var regionIndex = this.getRegionAtPosition(dragPosition); - var regionOriginIndex = this.getRegionAtPosition(this.mouseDragOrigin); - if (regionIndex == -1 || regionOriginIndex == -1) - return note; - - var region = this.viewRegions[regionIndex]; - var regionOrigin = this.viewRegions[regionOriginIndex]; - - var dragTick = this.getTickAtPosition(dragPosition); - var pitchOffset = Math.round((this.mouseDragOrigin.y - regionOrigin.y1 - (dragPosition.y - region.y1)) / this.NOTE_HEIGHT * 2); - var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin); - - if (this.mouseDragAction == "move") - { - return { - tick: Math.max(0, Math.min(this.songData.endTick - note.duration, note.tick + tickOffset)), - duration: note.duration, - pitch: Math.max(this.theory.getMinPitch(), Math.min(this.theory.getMaxPitch(), note.pitch + pitchOffset)) - }; - } - else if (this.mouseDragAction == "stretch") - { - var x1Proportion = (note.tick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - var x2Proportion = (note.tick + note.duration - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - var mouseProportion = (dragTick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - - var pivotDist = (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - var newX1 = Math.round((this.mouseStretchPivotTick + pivotDist * (x1Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; - var newX2 = Math.round((this.mouseStretchPivotTick + pivotDist * (x2Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; - - if (newX2 < newX1) - { - var temp = newX1; - newX1 = newX2; - newX2 = temp; - } - - return { - tick: Math.max(0, newX1), - duration: newX2 - newX1, - pitch: note.pitch - }; - } -} - - -SongEditor.prototype.getChordDragged = function(chord, dragPosition) -{ - var dragTick = this.getTickAtPosition(dragPosition); - var tickOffset = dragTick - this.getTickAtPosition(this.mouseDragOrigin); - - if (this.mouseDragAction == "move") - { - return { - tick: Math.max(0, Math.min(this.songData.endTick - chord.duration, chord.tick + tickOffset)), - duration: chord.duration - }; - } - else if (this.mouseDragAction == "stretch") - { - var x1Proportion = (chord.tick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - var x2Proportion = (chord.tick + chord.duration - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - var mouseProportion = (dragTick - this.mouseStretchPivotTick) / (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - - var pivotDist = (this.mouseStretchOriginTick - this.mouseStretchPivotTick); - var newX1 = Math.round((this.mouseStretchPivotTick + pivotDist * (x1Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; - var newX2 = Math.round((this.mouseStretchPivotTick + pivotDist * (x2Proportion * mouseProportion)) / this.tickSnap) * this.tickSnap; - - if (newX2 < newX1) - { - var temp = newX1; - newX1 = newX2; - newX2 = temp; - } - - return { - tick: Math.max(0, newX1), - duration: newX2 - newX1 - }; - } -} - - -SongEditor.prototype.getKeyChangeDragged = function(keyChange, dragPosition) -{ - var tickOffset = this.getTickAtPosition(dragPosition) - this.getTickAtPosition(this.mouseDragOrigin); - - if (this.mouseDragAction == "move") - return { tick: Math.max(0, Math.min(this.songData.endTick, keyChange.tick + tickOffset)) }; - - // TODO: Should move proportionally, like notes. - else if (this.mouseDragAction == "stretch") - return { tick: keyChange.tick }; -} - - -SongEditor.prototype.getMeterChangeDragged = function(meterChange, dragPosition) -{ - var tickOffset = this.getTickAtPosition(dragPosition) - this.getTickAtPosition(this.mouseDragOrigin); - - if (this.mouseDragAction == "move") - return { tick: Math.max(0, Math.min(this.songData.endTick, meterChange.tick + tickOffset)) }; - - // TODO: Should move proportionally, like notes. - else if (this.mouseDragAction == "stretch") - return { tick: meterChange.tick }; -} - - -SongEditor.prototype.getSectionKnobDraggedTick = function(region, dragPosition) -{ - var tickOffset = this.getTickAtPositionAtRegion(region, dragPosition) - this.getTickAtPosition(this.mouseDragOrigin); - - return Math.max(region.sectionTick, region.sectionTick + region.sectionDuration + tickOffset); -} \ No newline at end of file diff --git a/src/editor_representation.js b/src/editor_representation.js deleted file mode 100644 index 26ff607..0000000 --- a/src/editor_representation.js +++ /dev/null @@ -1,271 +0,0 @@ -// Sets up representation objects for the data in the song, -// which are used to draw the staff and for interaction with the mouse. -SongEditor.prototype.refreshRepresentation = function() -{ - // Clear representation objects. - this.viewRegions = []; - - // Clear selection arrays and push a boolean false for each object in the song data, - // effectively unselecting everything, while also accomodating added/removed objects. - this.selectedObjects = 0; - - this.noteSelections = []; - for (var i = 0; i < this.songData.notes.length; i++) - this.noteSelections.push(false); - - this.chordSelections = []; - for (var i = 0; i < this.songData.chords.length; i++) - this.chordSelections.push(false); - - this.keyChangeSelections = []; - for (var i = 0; i < this.songData.keyChanges.length; i++) - this.keyChangeSelections.push(false); - - this.meterChangeSelections = []; - for (var i = 0; i < this.songData.meterChanges.length; i++) - this.meterChangeSelections.push(false); - - // Iterate through song sections. - var maxX = this.MARGIN_LEFT; - var currentY = this.MARGIN_TOP; - var currentKeyChangeIndex = -1; - var currentMeterChangeIndex = -1; - - if (this.songData.keyChanges.length > 0 && - this.songData.keyChanges[0].tick == 0) - currentKeyChangeIndex = 0; - - if (this.songData.meterChanges.length > 0 && - this.songData.meterChanges[0].tick == 0) - currentMeterChangeIndex = 0; - - var DEFAULT_KEY = new SongDataKeyChange(-1, this.theory.scales[0], this.theory.C); - var DEFAULT_METER = new SongDataMeterChange(-1, 4, 4); - - for (var i = 0; i < this.songData.sectionBreaks.length + 1; i++) - { - var sectionTickRange = this.songData.getSectionTickRange(i); - var sectionDuration = sectionTickRange.end - sectionTickRange.start; - - var lowestNoteRow = 7 * 5; - var highestNoteRow = 7 * 6; - - // Create regions with non-changing key and meter. - var regions = []; - var currentRegionStart = sectionTickRange.start; - while (currentRegionStart < sectionTickRange.end) - { - // Find next key change tick and next meter change tick. - var nextKeyChangeTick = sectionTickRange.end; - var nextMeterChangeTick = sectionTickRange.end; - var willChangeKey = false; - var willChangeMeter = false; - - if (currentKeyChangeIndex + 1 < this.songData.keyChanges.length && - this.songData.keyChanges[currentKeyChangeIndex + 1].tick < nextKeyChangeTick) - { - nextKeyChangeTick = this.songData.keyChanges[currentKeyChangeIndex + 1].tick; - willChangeKey = true; - } - - if (currentMeterChangeIndex + 1 < this.songData.meterChanges.length && - this.songData.meterChanges[currentMeterChangeIndex + 1].tick < nextMeterChangeTick) - { - nextMeterChangeTick = this.songData.meterChanges[currentMeterChangeIndex + 1].tick; - willChangeMeter = true; - } - - var regionEndTick = Math.min(nextKeyChangeTick, nextMeterChangeTick); - var key = (currentKeyChangeIndex >= 0 ? this.songData.keyChanges[currentKeyChangeIndex] : DEFAULT_KEY); - var meter = (currentMeterChangeIndex >= 0 ? this.songData.meterChanges[currentMeterChangeIndex] : DEFAULT_METER); - - // Find lowest and highest pitch in section. - var notes = []; - for (var n = 0; n < this.songData.notes.length; n++) - { - var note = this.songData.notes[n]; - - if (note.tick < sectionTickRange.end && - note.tick + note.duration >= sectionTickRange.start) - { - var noteRow = this.theory.getRowForPitch(note.pitch, key.scale, key.tonicPitch); - lowestNoteRow = Math.min(lowestNoteRow, Math.floor(noteRow)); - highestNoteRow = Math.max(highestNoteRow, Math.ceil(noteRow) + 1); - notes.push(n); - } - } - - // Find chords in section. - var chords = []; - for (var n = 0; n < this.songData.chords.length; n++) - { - var chord = this.songData.chords[n]; - - if (chord.tick < sectionTickRange.end && - chord.tick + chord.duration >= sectionTickRange.start) - { - chords.push(n); - } - } - - var keyChanges = []; - if (key.tick == currentRegionStart) - keyChanges.push(currentKeyChangeIndex); - - var meterChanges = []; - if (meter.tick == currentRegionStart) - meterChanges.push(currentMeterChangeIndex); - - // Add region to list. - var region = { - section: i, - sectionTick: sectionTickRange.start, - sectionDuration: sectionDuration, - tick: currentRegionStart, - duration: regionEndTick - currentRegionStart, - x1: this.MARGIN_LEFT + (currentRegionStart - sectionTickRange.start) * this.tickZoom, - y1: 0, - x2: this.MARGIN_LEFT + (regionEndTick - sectionTickRange.start) * this.tickZoom, - y2: 0, - - interactBBox: { - x1: this.MARGIN_LEFT + (currentRegionStart - sectionTickRange.start) * this.tickZoom, - y1: 0, - x2: this.MARGIN_LEFT + (regionEndTick - sectionTickRange.start) * this.tickZoom, - y2: 0, - }, - - key: key, - meter: meter, - notes: notes, - chords: chords, - keyChanges: keyChanges, - meterChanges: meterChanges, - sectionKnob: false - }; - - maxX = Math.max(maxX, region.x2 + this.MARGIN_RIGHT); - - regions.push(region); - this.viewRegions.push(region); - - // Prepare for the next region. - currentRegionStart = regionEndTick; - - if (nextKeyChangeTick == nextMeterChangeTick && willChangeKey && willChangeMeter) - { - currentKeyChangeIndex++; - currentMeterChangeIndex++; - } - else if (nextKeyChangeTick < nextMeterChangeTick && willChangeKey) - { - currentKeyChangeIndex++; - } - else if (nextMeterChangeTick < nextKeyChangeTick && willChangeMeter) - { - currentMeterChangeIndex++; - } - } - - // Prepare for the next section. - var sectionHeight = - this.HEADER_HEIGHT + - (highestNoteRow - lowestNoteRow) * this.NOTE_HEIGHT + - this.CHORD_NOTE_SEPARATION + this.CHORD_HEIGHT; - - for (var r = 0; r < regions.length; r++) - { - regions[r].lowestNoteRow = lowestNoteRow; - regions[r].highestNoteRow = highestNoteRow; - regions[r].y1 = currentY; - regions[r].y2 = currentY + sectionHeight; - - regions[r].interactBBox.y1 = currentY; - regions[r].interactBBox.y2 = currentY + sectionHeight + this.SECTION_SEPARATION; - - if (r == 0) - regions[r].interactBBox.x1 = 0; - - if (r == regions.length - 1) - { - regions[r].sectionKnob = true; - regions[r].interactBBox.x2 = this.canvasWidth; - } - } - - currentY += sectionHeight + this.SECTION_SEPARATION; - } - - this.canvas.width = maxX + 400; - this.canvas.height = currentY; -} - - -SongEditor.prototype.getNotePosition = function(region, row, tick, duration) -{ - var x1 = region.x1 + (tick - region.tick) * this.tickZoom; - var y1 = region.y2 - this.CHORD_HEIGHT - this.CHORD_NOTE_SEPARATION - (row - region.lowestNoteRow + 1) * this.NOTE_HEIGHT; - - return { - x1: x1, - y1: y1, - x2: x1 + duration * this.tickZoom, - y2: y1 + this.NOTE_HEIGHT - }; -} - - -SongEditor.prototype.getChordPosition = function(region, tick, duration) -{ - var x1 = region.x1 + (tick - region.tick) * this.tickZoom; - var y1 = region.y2 - this.CHORD_HEIGHT; - - return { - x1: x1, - y1: y1, - x2: x1 + duration * this.tickZoom, - y2: y1 + this.CHORD_HEIGHT - }; -} - - -SongEditor.prototype.getKeyChangePosition = function(region, tick) -{ - var x1 = region.x1 + (tick - region.tick) * this.tickZoom; - var y1 = region.y1; - - return { - x1: x1, - y1: y1, - x2: region.x2, - y2: y1 + this.HEADER_LINE_HEIGHT - }; -} - - -SongEditor.prototype.getMeterChangePosition = function(region, tick) -{ - var x1 = region.x1 + (tick - region.tick) * this.tickZoom; - var y1 = region.y1 + this.HEADER_LINE_HEIGHT; - - return { - x1: x1, - y1: y1, - x2: region.x2, - y2: y1 + this.HEADER_LINE_HEIGHT - }; -} - - -SongEditor.prototype.getSectionKnobPosition = function(region) -{ - var x1 = region.x1 + (region.duration) * this.tickZoom - 12; - var y1 = region.y1; - - return { - x1: x1, - y1: y1, - x2: x1 + 24, - y2: y1 + this.HEADER_HEIGHT - }; -} \ No newline at end of file diff --git a/src/song/meter.js b/src/song/meter.js new file mode 100644 index 0000000..3febddd --- /dev/null +++ b/src/song/meter.js @@ -0,0 +1,6 @@ +function Meter(time, numerator, denominator) +{ + this.time = time; + this.numerator = numerator; + this.denominator = denominator; +} \ No newline at end of file diff --git a/src/song/note.js b/src/song/note.js new file mode 100644 index 0000000..746de7b --- /dev/null +++ b/src/song/note.js @@ -0,0 +1,5 @@ +function Note(timeRange, pitch) +{ + this.timeRange = timeRange; + this.pitch = pitch; +} \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js new file mode 100644 index 0000000..c6b2bf6 --- /dev/null +++ b/src/song/song.js @@ -0,0 +1,105 @@ +function Song() +{ + this.timePerWholeNote = 960; + this.length = 960 * 4; + + this.MIN_VALID_MIDI_PITCH = 3 * 12; + this.MAX_VALID_MIDI_PITCH = 8 * 12; + + this.notesById = []; + this.metersById = []; + + this.eventNoteAdded = new Callback(); + this.eventNoteChanged = new Callback(); + this.eventNoteRemoved = new Callback(); + + this.eventMeterAdded = new Callback(); + this.eventMeterChanged = new Callback(); + this.eventMeterRemoved = new Callback(); + + this.sanitize(); +} + + +Song.prototype.sanitize = function() +{ + this.meterAdd(new Meter(0, 4, 4)); +} + + +Song.prototype.raiseAllAdded = function() +{ + for (var i = 0; i < this.notesById.length; i++) + { + if (this.notesById[i] != null) + this.eventNoteAdded.call(function (fn) { fn(i); }); + } + + for (var i = 0; i < this.metersById.length; i++) + { + if (this.metersById[i] != null) + this.eventMeterAdded.call(function (fn) { fn(i); }); + } +} + + +Song.prototype.raiseAllRemoved = function() +{ + for (var i = 0; i < this.notesById.length; i++) + { + if (this.notesById[i] != null) + this.eventNoteRemoved.call(function (fn) { fn(i); }); + } + + for (var i = 0; i < this.metersById.length; i++) + { + if (this.metersById[i] != null) + this.eventMeterRemoved.call(function (fn) { fn(i); }); + } +} + + +Song.prototype.noteAdd = function(note) +{ + var id = this.notesById.length; + this.notesById[id] = note; + + this.eventNoteAdded.call(function (fn) { fn(id); }); + return id; +} + + +Song.prototype.noteRemove = function(id) +{ + this.eventNoteRemoved.call(function (fn) { fn(id); }); + this.notesById[id] = null; +} + + +Song.prototype.noteGet = function(id) +{ + return this.notesById[id]; +} + + +Song.prototype.meterAdd = function(meter) +{ + var id = this.metersById.length; + this.metersById[id] = meter; + + this.eventMeterAdded.call(function (fn) { fn(id); }); + return id; +} + + +Song.prototype.meterRemove = function(id) +{ + this.eventMeterRemoved.call(function (fn) { fn(id); }); + this.metersById[id] = null; +} + + +Song.prototype.meterGet = function(id) +{ + return this.metersById[id]; +} \ No newline at end of file diff --git a/src/songdata.js b/src/songdata.js deleted file mode 100644 index d78200f..0000000 --- a/src/songdata.js +++ /dev/null @@ -1,542 +0,0 @@ -function SongData(theory) -{ - this.theory = theory; - this.clear(); -} - - -function SongDataNote(tick, duration, pitch) -{ - this.tick = tick; - this.duration = duration; - this.pitch = pitch; -} - - -function SongDataChord(tick, duration, chord, rootPitch) -{ - this.tick = tick; - this.duration = duration; - this.chord = chord; - this.rootPitch = rootPitch; -} - - -function SongDataKeyChange(tick, scale, tonicPitch) -{ - this.tick = tick; - this.scale = scale; - this.tonicPitch = tonicPitch; -} - - -function SongDataMeterChange(tick, numerator, denominator) -{ - this.tick = tick; - this.numerator = numerator; - this.denominator = denominator; -} - - -function SongSectionBreak(tick) -{ - this.tick = tick; -} - - -function arrayAddSortedByTick(arr, obj) -{ - // TODO: Use binary search to avoid iterating through the entire array. - for (var i = 0; i < arr.length; i++) - { - var otherObj = arr[i]; - - if (otherObj.tick > obj.tick) - { - arr.splice(i, 0, obj); - return; - } - } - - arr.push(obj); -} - - -// Clears song data. -SongData.prototype.clear = function() -{ - this.beatsPerMinute = 120; - this.ticksPerWholeNote = 960; - this.endTick = this.ticksPerWholeNote * 4; - this.notes = []; - this.chords = []; - this.keyChanges = []; - this.meterChanges = []; - this.sectionBreaks = []; - - this.addKeyChange(new SongDataKeyChange(0, this.theory.scales[0], this.theory.C)); - this.addMeterChange(new SongDataMeterChange(0, 4, 4)); -} - - -// Returns whether the given tick value is valid. -SongData.prototype.isValidTick = function(tick) -{ - // Arbitrary upper limit. - return (tick >= 0 && tick < 1000000 && (tick % 1) == 0); -} - - -// Returns whether the given duration value is valid. -SongData.prototype.isValidDuration = function(duration) -{ - // Arbitrary upper limit. - return (duration > 0 && duration < 10000 && (duration % 1) == 0); -} - - -// Returns whether the given pitch value is valid. -SongData.prototype.isValidPitch = function(pitch) -{ - return (pitch >= this.theory.getMinPitch() && pitch <= this.theory.getMaxPitch() && (pitch % 1) == 0) -} - - -// Adds the given note to the data, and returns whether it was successful. -SongData.prototype.addNote = function(note) -{ - if (!this.isValidTick(note.tick) || !this.isValidDuration(note.duration) || !this.isValidPitch(note.pitch)) - return false; - - // Clip notes which were at the same range. - this.removeNotesByTickRange(note.tick, note.tick + note.duration, note.pitch); - arrayAddSortedByTick(this.notes, note); - return true; -} - - -// Adds the given chord to the data, and returns whether it was successful. -SongData.prototype.addChord = function(chord) -{ - if (!this.isValidTick(chord.tick) || !this.isValidDuration(chord.duration)) - return false; - - // Clip chords which were at the same range. - this.removeChordsByTickRange(chord.tick, chord.tick + chord.duration); - arrayAddSortedByTick(this.chords, chord); - return true; -} - - -// Adds the given key change to the data, and returns whether it was successful. -SongData.prototype.addKeyChange = function(keyChange) -{ - if (!this.isValidTick(keyChange.tick)) - return false; - - // Remove key changes which were at the same tick. - for (var i = this.keyChanges.length - 1; i >= 0; i--) - { - if (this.keyChanges[i].tick == keyChange.tick) - { - this.keyChanges.splice(i, 1); - } - } - - arrayAddSortedByTick(this.keyChanges, keyChange); - return true; -} - - -// Adds the given meter change to the data, and returns whether it was successful. -SongData.prototype.addMeterChange = function(meterChange) -{ - if (!this.isValidTick(meterChange.tick)) - return false; - - // Remove meter changes which were at the same tick. - for (var i = this.meterChanges.length - 1; i >= 0; i--) - { - if (this.meterChanges[i].tick == meterChange.tick) - { - this.meterChanges.splice(i, 1); - } - } - - arrayAddSortedByTick(this.meterChanges, meterChange); - return true; -} - - -// Adds a section break to the data, and returns whether it was successful. -SongData.prototype.addSectionBreak = function(sectionBreak) -{ - if (!this.isValidTick(sectionBreak.tick) || sectionBreak.tick == 0 || sectionBreak.tick == this.endTick) - return false; - - // Remove meter changes which were at the same tick. - for (var i = this.sectionBreaks.length - 1; i >= 0; i--) - { - if (this.sectionBreaks[i].tick == sectionBreak.tick) - { - this.sectionBreaks.splice(i, 1); - } - } - - arrayAddSortedByTick(this.sectionBreaks, sectionBreak); - return true; -} - - -// Remove or truncate notes that fall between tickBegin and tickEnd, optionally only if -// their pitches match the given pitch. Pass null for pitch to not consider pitches. -SongData.prototype.removeNotesByTickRange = function(tickBegin, tickEnd, pitch) -{ - for (var i = this.notes.length - 1; i >= 0; i--) - { - var otherNote = this.notes[i]; - - if (pitch != null && otherNote.pitch != pitch) - continue; - - if (otherNote.tick >= tickBegin && otherNote.tick + otherNote.duration <= tickEnd) - { - this.notes.splice(i, 1); - } - else if (otherNote.tick >= tickBegin && otherNote.tick < tickEnd && otherNote.tick + otherNote.duration > tickEnd) - { - var otherNoteEndTick = otherNote.tick + otherNote.duration; - otherNote.tick = tickEnd; - otherNote.duration = otherNoteEndTick - otherNote.tick; - } - else if (otherNote.tick < tickBegin && otherNote.tick + otherNote.duration >= tickBegin) - { - otherNote.duration = tickBegin - otherNote.tick; - } - } -} - - -// Remove or truncate chords that fall between tickBegin and tickEnd. -SongData.prototype.removeChordsByTickRange = function(tickBegin, tickEnd) -{ - for (var i = this.chords.length - 1; i >= 0; i--) - { - var otherChord = this.chords[i]; - - if (otherChord.tick >= tickBegin && otherChord.tick + otherChord.duration <= tickEnd) - { - this.chords.splice(i, 1); - } - else if (otherChord.tick >= tickBegin && otherChord.tick < tickEnd && otherChord.tick + otherChord.duration > tickEnd) - { - var otherNoteEndTick = otherChord.tick + otherChord.duration; - otherChord.tick = tickEnd; - otherChord.duration = otherNoteEndTick - otherChord.tick; - } - else if (otherChord.tick < tickBegin && otherChord.tick + otherChord.duration >= tickBegin) - { - otherChord.duration = tickBegin - otherChord.tick; - } - } -} - - -// Remove key changes that fall in the range ]tickBegin, tickEnd[. -SongData.prototype.removeKeyChangesByTickRange = function(tickBegin, tickEnd) -{ - for (var i = this.keyChanges.length - 1; i >= 0; i--) - { - var keyChange = this.keyChanges[i]; - - if (keyChange.tick > tickBegin && keyChange.tick < tickEnd) - this.keyChanges.splice(i, 1); - } -} - - -// Remove meter changes that fall in the range ]tickBegin, tickEnd[. -SongData.prototype.removeMeterChangesByTickRange = function(tickBegin, tickEnd) -{ - for (var i = this.meterChanges.length - 1; i >= 0; i--) - { - var meterChange = this.meterChanges[i]; - - if (meterChange.tick > tickBegin && meterChange.tick < tickEnd) - this.meterChanges.splice(i, 1); - } -} - - -// Split the given note in two, at the given tick. -SongData.prototype.splitNote = function(noteIndex, tick) -{ - var note = this.notes[noteIndex]; - - if (tick <= note.tick || tick >= note.tick + note.duration) - return; - - var noteClone = new SongDataNote(note.tick, note.duration, note.pitch); - - noteClone.tick = tick; - noteClone.duration = note.tick + note.duration - tick; - note.duration = tick - note.tick; - - this.notes.splice(noteIndex + 1, 0, noteClone); -} - - -// Split the given chord in two, at the given tick. -SongData.prototype.splitChord = function(chordIndex, tick) -{ - var chord = this.chords[chordIndex]; - - if (tick <= chord.tick || tick >= chord.tick + chord.duration) - return; - - var chordClone = new SongDataChord(chord.tick, chord.duration, chord.chord, chord.rootPitch); - - chordClone.tick = tick; - chordClone.duration = chord.tick + chord.duration - tick; - chord.duration = tick - chord.tick; - - this.chords.splice(chordIndex + 1, 0, chordClone); -} - - -// Inserts whitespace at the given tick, for the given duration, -// pushing forward any following elements, and returns whether successful. -SongData.prototype.insertWhitespace = function(tick, duration) -{ - if (!this.isValidTick(tick) || !this.isValidTick(tick + duration)) - return false; - - // Split any notes at the insertion tick. - for (var i = this.notes.length - 1; i >= 0; i--) - this.splitNote(i, tick); - - // Split any chords at the insertion tick. - for (var i = this.chords.length - 1; i >= 0; i--) - this.splitChord(i, tick); - - // Displace end tick. - this.endTick += duration; - - // Displace following notes. - for (var i = this.notes.length - 1; i >= 0; i--) - { - if (this.notes[i].tick >= tick) - this.notes[i].tick += duration; - } - - // Displace following chords. - for (var i = this.chords.length - 1; i >= 0; i--) - { - if (this.chords[i].tick >= tick) - this.chords[i].tick += duration; - } - - // Displace following key changes. - for (var i = this.keyChanges.length - 1; i >= 0; i--) - { - if (this.keyChanges[i].tick >= tick) - this.keyChanges[i].tick += duration; - } - - // Displace following meter changes. - for (var i = this.meterChanges.length - 1; i >= 0; i--) - { - if (this.meterChanges[i].tick >= tick) - this.meterChanges[i].tick += duration; - } - - // Displace following section breaks. - for (var i = this.sectionBreaks.length - 1; i >= 0; i--) - { - if (this.sectionBreaks[i].tick >= tick) - this.sectionBreaks[i].tick += duration; - } - - return true; -} - - -// Removes the region starting at the given tick, lasting the given duration, -// removing any contained elements, and returns whether successful. -SongData.prototype.remove = function(tick, duration) -{ - if (!this.isValidTick(tick) || !this.isValidTick(tick + duration)) - return false; - - // Remove any elements at the region. - this.removeNotesByTickRange(tick, tick + duration, null); - this.removeChordsByTickRange(tick, tick + duration); - this.removeKeyChangesByTickRange(tick, tick + duration); - this.removeMeterChangesByTickRange(tick, tick + duration); - - // TODO: Must sanitize elements after displacement. - - // Displace end tick. - this.endTick -= duration; - - // Displace following notes. - for (var i = this.notes.length - 1; i >= 0; i--) - { - if (this.notes[i].tick > tick) - this.notes[i].tick -= duration; - } - - // Displace following chords. - for (var i = this.chords.length - 1; i >= 0; i--) - { - if (this.chords[i].tick > tick) - this.chords[i].tick -= duration; - } - - // Displace following key changes. - for (var i = this.keyChanges.length - 1; i >= 0; i--) - { - if (this.keyChanges[i].tick > tick) - this.keyChanges[i].tick -= duration; - } - - // Displace following meter changes. - for (var i = this.meterChanges.length - 1; i >= 0; i--) - { - if (this.meterChanges[i].tick > tick) - this.meterChanges[i].tick -= duration; - } - - // Displace following section breaks. - for (var i = this.sectionBreaks.length - 1; i >= 0; i--) - { - if (this.sectionBreaks[i].tick > tick) - this.sectionBreaks[i].tick -= duration; - } - - return true; -} - - -// Gets the start and end ticks for the given section index. -SongData.prototype.getSectionTickRange = function(sectionIndex) -{ - var startTick = 0; - if (sectionIndex - 1 >= 0) - startTick = this.sectionBreaks[sectionIndex - 1].tick; - - var endTick = this.endTick; - if (sectionIndex < this.sectionBreaks.length) - endTick = this.sectionBreaks[sectionIndex].tick; - - return { start: startTick, end: endTick }; -} - - -// Returns a JSON string containing the song data. -SongData.prototype.save = function() -{ - var noteIndex = 0; - var chordIndex = 0; - var keyChangeIndex = 0; - var meterChangeIndex = 0; - - var json = "{\n"; - - json += " \"bpm\": " + this.beatsPerMinute + ",\n"; - json += " \"notes\": [\n"; - - for (var i = 0; i < this.notes.length; i++) - { - json += " "; - json += "[ " + this.notes[i].tick + ", "; - json += this.notes[i].duration + ", "; - json += this.notes[i].pitch + " ]"; - - if (i < this.notes.length - 1) - json += ","; - - json += "\n"; - } - - json += " ],\n \"chords\": [\n"; - - for (var i = 0; i < this.chords.length; i++) - { - json += " "; - json += "[ " + this.chords[i].tick + ", "; - json += this.chords[i].duration + ", "; - json += this.chords[i].rootPitch + ", "; - json += "{ \"chordId\": " + theory.getIdForChord(this.chords[i].chord) + " } ]"; - - if (i < this.chords.length - 1) - json += ","; - - json += "\n"; - } - - json += " ],\n \"keyChanges\": [\n"; - - for (var i = 0; i < this.keyChanges.length; i++) - { - json += " "; - json += "[ " + this.keyChanges[i].tick + ", "; - json += this.keyChanges[i].tonicPitch + ", "; - json += theory.getIdForScale(this.keyChanges[i].scale) + " ]"; - - if (i < this.keyChanges.length - 1) - json += ","; - - json += "\n"; - } - - json += " ],\n \"meterChanges\": [\n"; - - for (var i = 0; i < this.meterChanges.length; i++) - { - json += " "; - json += "[ " + this.meterChanges[i].tick + ", "; - json += this.meterChanges[i].numerator + ", "; - json += this.meterChanges[i].denominator + " ]"; - - if (i < this.meterChanges.length - 1) - json += ","; - - json += "\n"; - } - - json += " ]\n}"; - - return json; -} - - -// Loads the song data contained in the given string. -// Will throw an exception on error. -SongData.prototype.load = function(jsonStr) -{ - this.clear(); - - var song = JSON.parse(jsonStr); - this.beatsPerMinute = song.bpm; - - for (var i = 0; i < song.notes.length; i++) - { - this.addNote(new SongDataNote(song.notes[i][0], song.notes[i][1], song.notes[i][2])); - } - - for (var i = 0; i < song.chords.length; i++) - { - this.addChord(new SongDataChord(song.chords[i][0], song.chords[i][1], theory.getChordForId(song.chords[i][3].chordId), song.chords[i][2])); - } - - for (var i = 0; i < song.keyChanges.length; i++) - { - this.addKeyChange(new SongDataKeyChange(song.keyChanges[i][0], theory.getScaleForId(song.keyChanges[i][2]), song.keyChanges[i][1])); - } - - for (var i = 0; i < song.meterChanges.length; i++) - { - this.addMeterChange(new SongDataMeterChange(song.meterChanges[i][0], song.meterChanges[i][1], song.meterChanges[i][2])); - } -} \ No newline at end of file diff --git a/src/synth.js b/src/synth.js deleted file mode 100644 index 9c288c7..0000000 --- a/src/synth.js +++ /dev/null @@ -1,116 +0,0 @@ -function Synth() -{ - this.audioCtx = new AudioContext(); - this.globalVolume = 0.1; - this.voices = []; - this.delayedNotes = []; -} - - -Synth.prototype.process = function(deltaTime) -{ - for (var i = this.delayedNotes.length - 1; i >= 0; i--) - { - var note = this.delayedNotes[i]; - note.delay -= deltaTime; - - if (note.delay <= 0) - { - this.playNote(note.midiPitch, note.duration, note.volume); - this.delayedNotes.splice(i, 1); - } - } - - for (var i = this.voices.length - 1; i >= 0; i--) - { - var voice = this.voices[i]; - - voice.time += deltaTime; - voice.duration -= deltaTime; - - if (voice.time >= voice.duration) - { - this.stopVoice(i); - this.voices.splice(i, 1); - } - else - { - for (var j = 0; j < voice.gainNodes.length; j++) - { - voice.gainNodes[j].gain.value = voice.amplitudes[j] * voice.volume * (1 - voice.time / voice.duration) * this.globalVolume; - } - } - } -} - - -Synth.prototype.playNote = function(midiPitch, duration, volume) -{ - var pitchInHertz = Math.pow(2, (midiPitch - 69) / 12) * 440; - - var voice = { }; - voice.oscillators = []; - voice.gainNodes = []; - voice.volume = volume; - - voice.amplitudes = [ - 1.0, 0.399064778, 0.229404484, 0.151836061, - 0.196754229, 0.093742264, 0.060871957, - 0.138605419, 0.010535002, 0.071021868, - 0.029954614, 0.051299684, 0.055948288, - 0.066208224, 0.010067391, 0.00753679, - 0.008196947, 0.012955577, 0.007316738, - 0.006216476, 0.005116215, 0.006243983, - 0.002860679, 0.002558108, 0.0, 0.001650392]; - - for (var i = 1; i <= voice.amplitudes.length; i++) - { - var oscillator = this.audioCtx.createOscillator(); - oscillator.frequency.value = pitchInHertz * i; - oscillator.type = "sine"; - - var gainNode = this.audioCtx.createGain(); - oscillator.connect(gainNode); - gainNode.connect(this.audioCtx.destination); - gainNode.gain.value = voice.amplitudes[i - 1] * volume * this.globalVolume; - - oscillator.start(0); - - voice.oscillators.push(oscillator); - voice.gainNodes.push(gainNode); - } - - voice.time = 0; - voice.duration = duration; - this.voices.push(voice); -} - - -Synth.prototype.playNoteDelayed = function(midiPitch, delay, duration, volume) -{ - this.delayedNotes.push({ - midiPitch: midiPitch, - delay: delay, - duration: duration, - volume: volume - }); -} - - -Synth.prototype.stopAll = function() -{ - for (var i = this.voices.length - 1; i >= 0; i--) - { - this.stopVoice(i); - } - - this.voices = []; -} - - -Synth.prototype.stopVoice = function(index) -{ - var voice = this.voices[index]; - for (var i = 0; i < voice.oscillators.length; i++) - voice.oscillators[i].stop(0); -} \ No newline at end of file diff --git a/src/theory.js b/src/theory.js deleted file mode 100644 index 652f9aa..0000000 --- a/src/theory.js +++ /dev/null @@ -1,342 +0,0 @@ -function Theory() -{ - var that = this; - - var C = 0; - var Cs = 1; - var D = 2; - var Ds = 3; - var E = 4; - var F = 5; - var Fs = 6; - var G = 7; - var Gs = 8; - var A = 9; - var As = 10; - var B = 11; - - - this.C = C; - this.Cs = Cs; - this.D = D; - this.Ds = Ds; - this.E = E; - this.F = F; - this.Fs = Fs; - this.G = G; - this.Gs = Gs; - this.A = A; - this.As = As; - this.B = B; - - - // Scale without pitches will be generated below. - this.scaleGroups = - [ "Modes of Major", "Modes of Double Harmonic", "Modes of Phrygian Dominant" ]; - - this.scales = - [ - { name: "Major", group: 0, pitches: [ C, D, E, F, G, A, B ] }, - { name: "Dorian", group: 0, pitches: [] }, - { name: "Phrygian", group: 0, pitches: [] }, - { name: "Lydian", group: 0, pitches: [] }, - { name: "Mixolydian", group: 0, pitches: [] }, - { name: "Natural Minor", group: 0, pitches: [] }, - { name: "Locrian", group: 0, pitches: [] }, - - { name: "Harmonic Minor", group: 0, pitches: [ C, D, Ds, F, G, Gs, B ] }, - - { name: "Double Harmonic", group: 1, pitches: [ C, Cs, E, F, G, Gs, B ] }, - { name: "Lydian ♯2 ♯6", group: 1, pitches: [] }, - { name: "Ultraphrygian", group: 1, pitches: [] }, - { name: "Hungarian Minor", group: 1, pitches: [] }, - { name: "Oriental", group: 1, pitches: [] }, - { name: "Ionian Augmented ♯2", group: 1, pitches: [] }, - { name: "Locrian ♭♭3 ♭♭7", group: 1, pitches: [] }, - - { name: "Phrygian Dominant", group: 2, pitches: [ C, Cs, E, F, G, Gs, As ] }, - ]; - - // Generate the other modes of the non-null scales. - for (var i = 0; i < this.scales.length; i++) - { - if (this.scales[i].pitches.length == 0) - { - var offset = this.scales[i - 1].pitches[1]; - for (var j = 0; j < 7; j++) - { - this.scales[i].pitches[j] = (this.scales[i - 1].pitches[(j + 1) % 7] + 12 - offset) % 12; - } - } - } - - // Generate the mapping from pitch to scale degree. - for (var i = 0; i < this.scales.length; i++) - { - this.scales[i].pitchToDegreeMap = []; - - var curPitch = 0; - for (var j = 0; j < 7; j++) - { - while (curPitch < this.scales[i].pitches[j]) - { - this.scales[i].pitchToDegreeMap.push(j - 0.5); - curPitch++; - } - - this.scales[i].pitchToDegreeMap.push(j); - curPitch++; - } - - while (curPitch < 12) - { - this.scales[i].pitchToDegreeMap.push(6.5); - curPitch++; - } - } - - - this.chords = - [ - { name: "Major", uppercase: true, symbolSup: "", symbolSub: "", pitches: [ C, E, G ] }, - { name: "Minor", uppercase: false, symbolSup: "", symbolSub: "", pitches: [ C, Ds, G ] }, - { name: "Diminished", uppercase: false, symbolSup: "o", symbolSub: "", pitches: [ C, Ds, Fs ] }, - { name: "Augmented", uppercase: true, symbolSup: "+", symbolSub: "", pitches: [ C, E, Gs ] }, - { name: "Flat Fifth(?)", uppercase: true, symbolSup: "(♭5)", symbolSub: "", pitches: [ C, E, Fs ] }, - { name: "?", uppercase: true, symbolSup: "?", symbolSub: "", pitches: [ C, D, Fs ] }, - { name: "Dominant Seventh", uppercase: true, symbolSup: "7", symbolSub: "", pitches: [ C, E, G, As ] }, - { name: "Major Seventh", uppercase: true, symbolSup: "M7", symbolSub: "", pitches: [ C, E, G, B ] }, - { name: "Minor Seventh", uppercase: false, symbolSup: "7", symbolSub: "", pitches: [ C, Ds, G, As ] }, - { name: "Minor-Major Seventh", uppercase: true, symbolSup: "m(M7)", symbolSub: "", pitches: [ C, Ds, G, B ] }, - { name: "Diminished Seventh", uppercase: false, symbolSup: "o7", symbolSub: "", pitches: [ C, Ds, G, A ] }, - { name: "Half-Diminished Seventh", uppercase: false, symbolSup: "ø7", symbolSub: "", pitches: [ C, Ds, Fs, As ] }, - { name: "Augmented Seventh", uppercase: true, symbolSup: "+7", symbolSub: "", pitches: [ C, E, Gs, As ] }, - { name: "Augmented Major Seventh", uppercase: true, symbolSup: "+(M7)", symbolSub: "", pitches: [ C, E, Gs, B ] } - ]; - - - this.chordEmbelishments = - [ - { name: "Suspended Second", symbol: "sus2" }, - { name: "Suspended Fourth", symbol: "sus4" }, - { name: "Added Ninth", symbol: "add9" } - ]; - - - this.getMinPitch = function() - { - return 60 - 12 * 2; - } - - - this.getMaxPitch = function() - { - return 60 + 12 * 3 - 1; - } - - - this.getNameForPitch = function(pitch, scale, tonicPitch) - { - // TODO: Take the scale also into consideration. - var notes = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; - return notes[pitch % 12]; - }; - - - this.getRomanNumeralForPitch = function(pitch, scale, tonicPitch) - { - // TODO: Take the scale also into consideration for naming. - var numerals = ["I", "♭II", "II", "♭III", "III", "IV", "♭V", "V", "♭VI", "VI", "♭VII", "VII"]; - return numerals[(pitch + 12 - tonicPitch) % 12]; - }; - - - this.getIdForChord = function(chord) - { - return this.chords.indexOf(chord); - } - - - this.getChordForId = function(id) - { - return this.chords[id]; - } - - - this.getIdForScale = function(scale) - { - return this.scales.indexOf(scale); - } - - - this.getScaleForId = function(id) - { - return this.scales[id]; - } - - - this.getOctaveForPitch = function(pitch) - { - return Math.floor(pitch / 12); - }; - - - this.getOctaveForDegree = function(degree) - { - return Math.floor(degree / 7); - }; - - - var colors = - [ - "#ff0000", - "#ffb014", - "#efe600", - "#00d300", - "#4800ff", - "#b800e5", - "#ff00cb" - ]; - - this.getColorForDegree = function(degree) - { - return colors[degree]; - }; - - - // Returns the scale degree of the given pitch, according to the given key. - // May return fractional values, which indicates that the pitch falls - // between scale degrees. - this.getDegreeForPitch = function(pitch, scale, tonicPitch) - { - return scale.pitchToDegreeMap[(pitch + 12 - tonicPitch) % 12]; - }; - - - // Returns the row index where a note of the given pitch would be placed, - // according to the given key. May return fractional values, which - // indicates that the pitch falls between scale degrees. - this.getRowForPitch = function(pitch, scale, tonicPitch) - { - var pitchOctave = that.getOctaveForPitch(pitch - tonicPitch); - var pitchDegree = that.getDegreeForPitch(pitch, scale, tonicPitch); - - var offset = 0; - if (tonicPitch != 0) - offset = Math.floor(this.getRowForPitch(tonicPitch, scale, 0)); - - return pitchDegree + offset + pitchOctave * 7; - }; - - - // Returns the pitch of the given row index, according to the given key. - this.getPitchForRow = function(row, scale, tonicPitch) - { - var rowOctave = Math.floor(row / 7); - - var offset = 0; - if (tonicPitch != 0) - offset = Math.floor(that.getRowForPitch(tonicPitch, scale, 0)); - - return scale.pitches[Math.floor(row + 7 - offset) % 7] + tonicPitch + Math.floor((row - offset) / 7) * 12; - }; - - - // Returns a new chord altered by the given embelishments. - this.getEmbelishedChord = function(chordIndex, embelishmentIndices) - { - var newChord = { - name: this.chords[chordIndex].name, - uppercase: this.chords[chordIndex].uppercase, - symbolSup: this.chords[chordIndex].symbolSup, - symbolSub: this.chords[chordIndex].symbolSub, - pitches: this.chords[chordIndex].pitches.slice(0) - }; - - var hasSus2 = (embelishmentIndices.indexOf(0) >= 0); - var hasSus4 = (embelishmentIndices.indexOf(1) >= 0); - var hasAdd9 = (embelishmentIndices.indexOf(2) >= 0); - - if (hasAdd9) - { - newChord.pitches.splice(1, 0, D); - newChord.symbolSup += "(add9)"; - } - else if (hasSus2 && hasSus4) - { - newChord.pitches.splice(1, 1, D, F); - newChord.symbolSub += "sus24"; - } - else if (hasSus2) - { - newChord.pitches[1] = D; - newChord.symbolSub += "sus2"; - } - else if (hasSus4) - { - newChord.pitches[1] = F; - newChord.symbolSub += "sus4"; - } - - return newChord; - } - - - // Returns the first chord index in the chord array that fits the given pitches. - this.getFirstFittingChordIndexForPitches = function(pitches) - { - for (var i = 0; i < this.chords.length; i++) - { - var chord = this.chords[i]; - - if (pitches.length != chord.pitches.length) - continue; - - var offset = pitches[0]; - var match = true; - for (var j = 0; j < chord.pitches.length; j++) - { - if (chord.pitches[j] != pitches[j] - offset) - { - match = false; - break; - } - } - - if (match) - return i; - } - - console.log("Missing chord data for pitches:"); - console.log(pitches); - return null; - } - - - // Plays a sample of the given note. - this.playNoteSample = function(synth, pitch) - { - synth.playNote(pitch, 960 / 8, 1); - } - - - // Plays a sample of the given chord. - this.playChordSample = function(synth, chord, rootPitch) - { - for (var k = 0; k < chord.pitches.length; k++) - { - synth.playNote((rootPitch + chord.pitches[k]) % 12 + 60, 960 / 8, 0.75); - } - } - - - // Plays a sample of the given scale. - this.playScaleSample = function(synth, scale, tonicPitch) - { - for (var k = 0; k < scale.pitches.length; k++) - { - synth.playNoteDelayed((tonicPitch + scale.pitches[k]) + 60, k * 60, 960 / 8, 1); - } - synth.playNoteDelayed((tonicPitch + scale.pitches[0] + 12) + 60, scale.pitches.length * 60, 960 / 8, 1); - } -} diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js new file mode 100644 index 0000000..618c0df --- /dev/null +++ b/src/timeline/timeline.js @@ -0,0 +1,319 @@ +function Timeline(canvas) +{ + var that = this; + + // Set up constants. + this.INTERACT_NONE = 0x0; + this.INTERACT_MOVE_TIME = 0x1; + this.INTERACT_MOVE_PITCH = 0x2; + this.INTERACT_STRETCH_TIME = 0x4; + + // Get canvas context. + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); + + this.canvasWidth = 0; + this.canvasHeight = 0; + + // Set up mouse/keyboard interaction. + this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; + this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; + this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; + //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; + + this.mouseDown = false; + this.mouseDownPos = null; + this.mouseAction = this.INTERACT_NONE; + this.mouseMoveTime = 0; + this.mouseMovePitch = 0; + + this.hoverElement = null; + this.hoverRegion = null; + this.selectedElements = []; + + // Set up song and song events. + this.song = null; + + this.eventNoteAdded = new Callback(); + this.eventNoteChanged = new Callback(); + this.eventNoteRemoved = new Callback(); + + // Set up display metrics. + this.scrollTime = 0; + this.timeToPixelsScaling = 100 / 960; + this.noteHeight = 10; + + this.redrawDirtyTimeMin = -1; + this.redrawDirtyTimeMax = -1; + + // Set up tracks. + this.trackNotes = new TrackNotes(this); + + this.tracks = []; + this.tracks.push(this.trackNotes); +} + + +Timeline.prototype.setSong = function(song) +{ + this.unselectAll(); + + if (this.song != null) + this.song.raiseAllRemoved(); + + this.song = song; + + if (this.song != null) { + var that = this; + this.song.eventNoteAdded .add(function (id) { that.eventNoteAdded .call(function (fn) { fn(id); }); }); + this.song.eventNoteChanged.add(function (id) { that.eventNoteChanged.call(function (fn) { fn(id); }); }); + this.song.eventNoteRemoved.add(function (id) { that.eventNoteRemoved.call(function (fn) { fn(id); }); }); + this.song.raiseAllAdded(); + this.markDirtyAll(); + } +} + + +Timeline.prototype.handleMouseDown = function(ev) +{ + var that = this; + + ev.preventDefault(); + + var ctrl = ev.ctrlKey; + var mousePos = mouseToClient(this.canvas, ev); + + if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) + this.unselectAll(); + + if (this.hoverElement != null) + { + this.select(this.hoverElement); + this.unselectAllOfDifferentKind(this.hoverElement.interactKind); + + this.mouseAction = this.hoverElement.interactKind; + } + else + this.mouseAction = this.INTERACT_NONE; + + this.mouseDown = true; + this.mouseDownPos = mousePos; + this.mouseMoveTime = 0; + this.redraw(); +} + + +Timeline.prototype.handleMouseMove = function(ev) +{ + var that = this; + + ev.preventDefault(); + + var mousePos = mouseToClient(this.canvas, ev); + var mouseTime = mousePos.x / this.timeToPixelsScaling; + + if (this.mouseDown) + { + var mouseTimeDelta = (mousePos.x - this.mouseDownPos.x) / this.timeToPixelsScaling; + + if (this.selectedElements.length != 0) + { + if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) + { + var allEncompasingTimeRange = this.selectedElements[0].timeRange.clone(); + var allEncompasingInteractTimeRange = this.selectedElements[0].interactTimeRange.clone(); + + for (var i = 1; i < this.selectedElements.length; i++) + { + allEncompasingTimeRange .merge(this.selectedElements[i].timeRange); + allEncompasingInteractTimeRange.merge(this.selectedElements[i].interactTimeRange); + } + + this.markDirty( + allEncompasingTimeRange.start + this.mouseMoveTime, + allEncompasingTimeRange.end + this.mouseMoveTime); + + this.mouseMoveTime = + Math.max(-allEncompasingInteractTimeRange.start, + Math.min(this.song.length - allEncompasingInteractTimeRange.end, + mouseTimeDelta)); + + this.markDirty( + allEncompasingTimeRange.start + this.mouseMoveTime, + allEncompasingTimeRange.end + this.mouseMoveTime); + } + } + } + else + { + if (this.hoverElement != null) + this.markDirtyElement(this.hoverElement); + + this.hoverElement = null; + this.hoverRegion = null; + + for (var i = 0; i < this.tracks.length; i++) + { + var track = this.tracks[i]; + + if (mousePos.y < track.y || mousePos.y >= track.y + track.height) + continue; + + track.elements.enumerateOverlappingTime(mouseTime, function (index, elem) + { + for (var e = 0; e < elem.regions.length; e++) + { + var region = elem.regions[e]; + + if (mousePos.x >= region.x && + mousePos.x <= region.x + region.width && + mousePos.y >= region.y + track.y && + mousePos.y <= region.y + region.height + track.y) + { + that.hoverElement = elem; + that.hoverRegion = region; + } + } + }); + } + + if (this.hoverElement != null) + this.markDirtyElement(this.hoverElement); + + var regionKind = this.INTERACT_NONE; + if (this.hoverRegion != null) + regionKind = this.hoverRegion.kind; + + if ((regionKind & this.INTERACT_MOVE_TIME != 0) || + (regionKind & this.INTERACT_MOVE_PITCH != 0)) + this.canvas.style.cursor = "pointer"; + else if (regionKind & this.INTERACT_STRETCH_TIME != 0) + this.canvas.style.cursor = "ew-resize" + else + this.canvas.style.cursor = "default"; + } + + this.redraw(); +} + + +Timeline.prototype.handleMouseUp = function(ev) +{ + ev.preventDefault(); + + var mousePos = mouseToClient(this.canvas, ev); + + this.mouseDown = false; + this.mouseAction = this.INTERACT_NONE; + this.redraw(); +} + + +Timeline.prototype.markDirtyAll = function() +{ + this.redrawDirtyTimeMin = -100; + this.redrawDirtyTimeMax = this.song.length + 100; +} + + +Timeline.prototype.markDirty = function(timeStart, timeEnd) +{ + if (this.redrawDirtyTimeMin == -1 || timeStart - 10 < this.redrawDirtyTimeMin) + this.redrawDirtyTimeMin = timeStart - 10; + + if (this.redrawDirtyTimeMax == -1 || timeEnd + 10 > this.redrawDirtyTimeMax) + this.redrawDirtyTimeMax = timeEnd + 10; +} + + +Timeline.prototype.markDirtyElement = function(elem) +{ + this.markDirty(elem.timeRange.start, elem.timeRange.end); +} + + +Timeline.prototype.unselectAll = function() +{ + for (var i = 0; i < this.selectedElements.length; i++) + { + this.selectedElements[i].selected = false; + this.markDirtyElement(this.selectedElements[i]); + } + + this.selectedElements = []; +} + + +Timeline.prototype.unselectAllOfDifferentKind = function(kind) +{ + for (var i = this.selectedElements.length - 1; i >= 0; i--) + { + if ((this.selectedElements[i].interactKind & kind) == 0) + { + this.selectedElements[i].selected = false; + this.markDirtyElement(this.selectedElements[i]); + this.selectedElements[i].splice(i, 1); + } + } +} + + +Timeline.prototype.select = function(elem) +{ + elem.selected = true; + this.selectedElements.push(elem); + this.markDirtyElement(elem); +} + + +Timeline.prototype.relayout = function() +{ + this.canvasWidth = parseFloat(this.canvas.width); + this.canvasHeight = parseFloat(this.canvas.height); + + this.trackNotes.y = 5; + this.trackNotes.height = this.canvasHeight - 10; + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].relayout(); +} + + +Timeline.prototype.redraw = function() +{ + if (this.redrawDirtyTimeMin == -1 || this.redrawDirtyTimeMax == -1) + return; + + this.ctx.save(); + + this.ctx.beginPath(); + this.ctx.rect( + this.redrawDirtyTimeMin * this.timeToPixelsScaling + 1, + 0, + (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin) * this.timeToPixelsScaling, + this.canvasHeight); + this.ctx.clip(); + + // Clear background. + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect( + this.redrawDirtyTimeMin * this.timeToPixelsScaling, + 0, + (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin) * this.timeToPixelsScaling, + this.canvasHeight); + + // Draw tracks. + for (var i = 0; i < this.tracks.length; i++) + { + this.ctx.save(); + this.ctx.translate(0, this.tracks[i].y); + + this.tracks[i].redraw(this.redrawDirtyTimeMin, this.redrawDirtyTimeMax); + + this.ctx.restore(); + } + + this.redrawDirtyTimeMin = -1; + this.redrawDirtyTimeMax = -1; + this.ctx.restore(); +} \ No newline at end of file diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js new file mode 100644 index 0000000..2ca15e5 --- /dev/null +++ b/src/timeline/track_notes.js @@ -0,0 +1,146 @@ +function TrackNotes(timeline) +{ + this.timeline = timeline; + + var that = this; + this.timeline.eventNoteAdded .add(function (id) { that.onNoteAdded(id); }); + this.timeline.eventNoteRemoved.add(function (id) { that.onNoteRemoved(id); }); + + this.y = 0; + this.height = 0; + + this.scrollMidiPitch = 0; + + this.elements = new MapByTimeRange(); +} + + +TrackNotes.prototype.onNoteAdded = function(id) +{ + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + + var note = this.timeline.song.noteGet(id); + + var elem = { + id: id, + selected: false, + timeRange: note.timeRange.clone(), + + regions: [], + interactKind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, + interactTimeRange: note.timeRange.clone(), + interactPitch: note.pitch.clone() + }; + + this.refreshElementRegions(elem); + this.elements.add(id, elem); +} + + +TrackNotes.prototype.onNoteRemoved = function(id) +{ + this.elements.remove(id); +} + + +TrackNotes.prototype.refreshElementRegions = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + + var note = this.timeline.song.noteGet(elem.id); + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, + x: note.timeRange.start * toPixels, + y: this.height - noteHeight * (note.pitch.midiPitch - minPitch) - noteHeight, + width: note.timeRange.duration() * toPixels, + height: noteHeight - 1 + } + ]; +} + + +TrackNotes.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (index, elem) + { + that.refreshElementRegions(elem); + }); +} + + +TrackNotes.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.song.MAX_VALID_MIDI_PITCH; + + var xMin = time1 * toPixels; + var xMax = Math.min(time2, this.timeline.song.length) * toPixels; + + ctx.save(); + + ctx.beginPath(); + ctx.rect(xMin, 0, xMax - xMin + 1, this.height + 1); + ctx.clip(); + + ctx.translate(0.5, 0.5); + + // Draw pitch rows. + ctx.fillStyle = "#e4e4e4"; + for (var i = minPitch; i <= maxPitch; i++) + { + ctx.fillRect( + xMin, + this.height - noteHeight * (i - minPitch), + xMax - xMin, + noteHeight - 0.5); + } + + // Draw notes. + this.elements.enumerateOverlappingRangeOrSelected(new TimeRange(time1, time2), function (index, elem) + { + var note = that.timeline.song.noteGet(elem.id); + + var start = note.timeRange.start; + var duration = note.timeRange.duration(); + if (elem.selected && (that.timeline.mouseAction & that.timeline.INTERACT_MOVE_TIME) != 0) + start += that.timeline.mouseMoveTime; + + ctx.globalAlpha = 1; + if (elem == that.timeline.hoverElement) + ctx.fillStyle = "#ff8888"; + else + ctx.fillStyle = "#ff0000"; + + var x = 0.5 + Math.floor(start * toPixels); + var y = 0.5 + that.height - noteHeight * (note.pitch.midiPitch - minPitch); + var w = Math.floor(duration * toPixels); + + ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + + if (elem.selected) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.5; + + ctx.fillRect(x, y - noteHeight + 2, w, noteHeight - 1 - 4); + } + }); + + // Draw borders. + ctx.strokeStyle = "#000000"; + ctx.strokeRect(0, 0, this.timeline.song.length * toPixels, this.height); + + ctx.restore(); +} \ No newline at end of file diff --git a/src/toolbox.js b/src/toolbox.js deleted file mode 100644 index d16ef98..0000000 --- a/src/toolbox.js +++ /dev/null @@ -1,657 +0,0 @@ -function Toolbox(div, editor, theory, synth) -{ - this.editor = editor; - this.theory = theory; - this.synth = synth; - - this.div = div; - this.playing = false; - this.playTimer = null; - this.playTimerRefresh = null; - - this.currentChordKind = 0; - - this.prepareControlsPanel(); - this.prepareKeyPanel(); - this.prepareMeterPanel(); - this.prepareChordsPanel(); - - - // Set up callbacks. - var that = this; - editor.addOnCursorChanged(function() { that.refresh(); }); - editor.addOnSelectionChanged(function() { that.refresh(); }); - - this.refresh(); - - /* - this.inputBPM.onchange = function() - { - var bpm = parseInt(that.inputBPM.value); - - if (bpm != bpm) bpm = 120; // Test for NaN. - if (bpm < 1) bpm = 1; - if (bpm > 400) bpm = 400; - bpm -= (bpm % 1); - - that.editor.songData.beatsPerMinute = bpm; - } - - this.inputBPM.onblur = function() - { - that.inputBPM.value = that.editor.songData.beatsPerMinute; - } - - this.buttonSave.onclick = function() - { - that.inputSave.value = that.editor.songData.save(); - } - - this.buttonLoad.onclick = function() - { - if (window.confirm("Overwrite the current song?")) - { - try - { - that.editor.songData.load(that.inputSave.value); - } - catch (err) - { - window.alert("There was an error with the input:\n\n" + err) - that.editor.songData.clear(); - } - - that.editor.cursorTick = 0; - that.editor.showCursor = true; - that.editor.cursorZone = that.editor.CURSOR_ZONE_ALL; - that.editor.unselectAll(); - that.editor.clearHover(); - that.editor.refreshRepresentation(); - that.editor.refreshCanvas(); - that.refresh(); - } - }*/ -} - - -Toolbox.prototype.prepareControlsPanel = function() -{ - var that = this; - document.getElementById("toolboxPlayButton").onclick = function() { that.togglePlay(); }; - document.getElementById("toolboxRewindButton").onclick = function() { that.rewind(); }; -} - - -Toolbox.prototype.prepareKeyPanel = function() -{ - var keyTonicSelect = document.getElementById("toolboxKeyTonicSelect"); - this.keyTonicOptions = []; - for (var i = 0; i < 12; i++) - { - this.keyTonicOptions[i] = document.createElement("option"); - keyTonicSelect.appendChild(this.keyTonicOptions[i]); - } - - var keyScaleSelect = document.getElementById("toolboxKeyScaleSelect"); - this.keyScaleOptions = []; - var group = -1; - var groupElement = null; - for (var i = 0; i < this.theory.scales.length; i++) - { - if (group != this.theory.scales[i].group) - { - group = this.theory.scales[i].group; - groupElement = document.createElement("optgroup"); - groupElement.label = this.theory.scaleGroups[group]; - keyScaleSelect.appendChild(groupElement); - } - - this.keyScaleOptions[i] = document.createElement("option"); - this.keyScaleOptions[i].innerHTML = this.theory.scales[i].name; - groupElement.appendChild(this.keyScaleOptions[i]); - } - - var that = this; - keyTonicSelect.onchange = function() { that.editKeyChange(); }; - keyScaleSelect.onchange = function() { that.editKeyChange(); }; - document.getElementById("toolboxKeyListenButton").onclick = function() { that.listenToKey(); }; - document.getElementById("toolboxKeyInsertButton").onclick = function() { that.insertKeyChange(); }; -} - - -Toolbox.prototype.prepareMeterPanel = function() -{ - var meterNumeratorSelect = document.getElementById("toolboxMeterNumeratorSelect"); - this.meterNumeratorOptions = []; - for (var i = 0; i < 20; i++) - { - this.meterNumeratorOptions[i] = document.createElement("option"); - this.meterNumeratorOptions[i].innerHTML = "" + (i + 1); - meterNumeratorSelect.appendChild(this.meterNumeratorOptions[i]); - } - - var meterDenominatorSelect = document.getElementById("toolboxMeterDenominatorSelect"); - this.meterDenominatorOptions = []; - this.meterDenominators = [ 2, 4, 8, 16 ]; - for (var i = 0; i < this.meterDenominators.length; i++) - { - this.meterDenominatorOptions[i] = document.createElement("option"); - this.meterDenominatorOptions[i].innerHTML = "" + this.meterDenominators[i]; - meterDenominatorSelect.appendChild(this.meterDenominatorOptions[i]); - } - - var that = this; - meterNumeratorSelect.onchange = function() { that.editMeterChange(); }; - meterDenominatorSelect.onchange = function() { that.editMeterChange(); }; - document.getElementById("toolboxMeterInsertButton").onclick = function() { that.insertMeterChange(); }; -} - - -Toolbox.prototype.prepareChordsPanel = function() -{ - var divKinds = document.getElementById("toolboxChordKindsDiv"); - var divEmbelishments = document.getElementById("toolboxChordEmbelishDiv"); - - var that = this; - var makeChangeKindFunction = function(index) - { - return function() - { - that.currentChordKind = index; - that.refreshChordsPanel(); - } - } - - this.chordOptions = []; - for (var i = 0; i < this.theory.chords.length + 1; i++) - { - this.chordOptions[i] = document.createElement("button"); - - var text; - if (i == 0) - text = "In Key"; - else - { - text = (this.theory.chords[i - 1].uppercase ? "I" : "i"); - text += "" + this.theory.chords[i - 1].symbolSup + ""; - text += "" + this.theory.chords[i - 1].symbolSub + ""; - } - - this.chordOptions[i].innerHTML = text; - this.chordOptions[i].className = "toolboxChordKindButton"; - this.chordOptions[i].onclick = makeChangeKindFunction(i); - - divKinds.appendChild(this.chordOptions[i]); - - if (i == 0 || - (i % Math.floor(this.theory.chords.length / 2)) == 0) - { - divKinds.appendChild(document.createElement("br")); - } - } - - this.currentChordKind = 0; - this.chordOptions[this.currentChordKind].style.border = "2px solid #ff0000"; - - this.chordEmbelishmentOptions = []; - for (var i = 0; i < this.theory.chordEmbelishments.length; i++) - { - var text = this.theory.chordEmbelishments[i].symbol; - - var label = document.createElement("label"); - label.className = "toolboxLabel"; - - this.chordEmbelishmentOptions[i] = document.createElement("input"); - this.chordEmbelishmentOptions[i].type = "checkbox"; - this.chordEmbelishmentOptions[i].onclick = function() { that.refreshChordsPanel(); }; - - label.appendChild(this.chordEmbelishmentOptions[i]); - label.appendChild(document.createTextNode(text)); - divEmbelishments.appendChild(label); - - divEmbelishments.appendChild(document.createElement("br")); - } -} - - -Toolbox.prototype.refreshKeyPanel = function() -{ - var div = document.getElementById("toolboxKeyDiv"); - div.style.visibility = "hidden"; - - var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); - if (keyChange != null) - { - var scaleIndex = 0; - for (var i = 0; i < this.theory.scales.length; i++) - { - if (keyChange.scale == this.theory.scales[i]) - { - scaleIndex = i; - break; - } - } - - document.getElementById("toolboxKeyScaleSelect").selectedIndex = scaleIndex; - - for (var i = 0; i < 12; i++) - { - this.keyTonicOptions[i].innerHTML = this.theory.getNameForPitch(i, keyChange.scale, keyChange.tonicPitch); - } - - document.getElementById("toolboxKeyTonicSelect").selectedIndex = keyChange.tonicPitch; - div.style.visibility = "visible"; - } -} - - -Toolbox.prototype.refreshMeterPanel = function() -{ - var div = document.getElementById("toolboxMeterDiv"); - div.style.visibility = "hidden"; - - var meterChange = this.editor.getMeterAtTick(this.editor.cursorTick); - if (meterChange != null) - { - document.getElementById("toolboxMeterNumeratorSelect").selectedIndex = meterChange.numerator - 1; - - var denominatorIndex = 0; - if (meterChange.denominator == 2) denominatorIndex = 0; - else if (meterChange.denominator == 4) denominatorIndex = 1; - else if (meterChange.denominator == 8) denominatorIndex = 2; - else if (meterChange.denominator == 16) denominatorIndex = 3; - document.getElementById("toolboxMeterDenominatorSelect").selectedIndex = denominatorIndex; - - div.style.visibility = "visible"; - } -} - - -Toolbox.prototype.refreshNotesPanel = function() -{ - var div = document.getElementById("toolboxNotesDiv"); - div.style.visibility = "hidden"; - - var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); - if (keyChange == null) - return; - - div.style.visibility = "visible"; - - var that = this; - for (var i = 0; i < 12; i++) - { - var button = document.getElementById("toolboxNote" + i); - - var pitch = (i + keyChange.tonicPitch) % 12; - button.innerHTML = this.theory.getNameForPitch(pitch, keyChange.scale, keyChange.tonicPitch); - - var degree = this.theory.getDegreeForPitch(pitch, keyChange.scale, keyChange.tonicPitch); - - if ((degree % 1) == 0) - { - var color = this.theory.getColorForDegree(degree); - button.style.background = color; - button.style.color = "#000000"; - } - else - { - button.style.background = "#dddddd"; - button.style.color = "#888888"; - } - - button.onclick = function(pitch) { var innerPitch = pitch; return function() { that.insertNote(innerPitch); }; }(pitch + 60); - } -} - - -Toolbox.prototype.refreshChordsPanel = function() -{ - var div = document.getElementById("toolboxChordsDiv"); - div.style.visibility = "hidden"; - - var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); - if (keyChange == null) - return; - - div.style.visibility = "visible"; - - var that = this; - var refreshChordButton = function(theory, button, key, chord, rootPitch) - { - var degree = theory.getDegreeForPitch(rootPitch, key.scale, key.tonicPitch); - var color = theory.getColorForDegree(degree); - var numeral = theory.getRomanNumeralForPitch(rootPitch, key.scale, key.tonicPitch); - - var romanText = (chord.uppercase ? numeral : numeral.toLowerCase()); - romanText += "" + chord.symbolSup + ""; - romanText += "" + chord.symbolSub + ""; - - button.innerHTML = romanText; - button.style.borderColor = color; - button.style.visibility = "visible"; - - if ((degree % 1) == 0) - { - button.style.borderColor = color; - button.style.color = "#000000"; - } - else - { - button.style.borderColor = "#dddddd"; - button.style.color = "#888888"; - } - - button.onclick = function() { that.insertChord(chord, rootPitch); }; - } - - for (var i = 0; i < 12; i++) - document.getElementById("toolboxChord" + i).style.visibility = "hidden"; - - - var embelishmentIndices = []; - for (var i = 0; i < this.chordEmbelishmentOptions.length; i++) - { - if (this.chordEmbelishmentOptions[i].checked) - embelishmentIndices.push(i); - } - - // "In Key" kind. - if (this.currentChordKind == 0) - { - for (var i = 0; i < 7; i++) - { - var pitch1 = keyChange.tonicPitch + keyChange.scale.pitches[i]; - - var pitch2 = keyChange.tonicPitch + keyChange.scale.pitches[(i + 2) % 7]; - if ((i + 2) >= 7) - pitch2 += 12; - - var pitch3 = keyChange.tonicPitch + keyChange.scale.pitches[(i + 4) % 7]; - if ((i + 4) >= 7) - pitch3 += 12; - - var chordIndex = this.theory.getFirstFittingChordIndexForPitches([pitch1, pitch2, pitch3]); - var chord = this.theory.getEmbelishedChord(chordIndex, embelishmentIndices); - - refreshChordButton(this.theory, document.getElementById("toolboxChord" + i), keyChange, chord, pitch1); - } - } - // Other chord kinds. - else - { - var inKeyIndex = 0; - var outOfKeyIndex = 7; - var buttonIndices = [0, 7, 1, 8, 2, 3, 9, 4, 10, 5, 11, 6]; - - for (var i = 0; i < 12; i++) - { - var rootPitch = (keyChange.tonicPitch + i) % 12; - var chordIndex = this.currentChordKind - 1; - var chord = this.theory.getEmbelishedChord(chordIndex, embelishmentIndices); - - // FIXME: Coloration for flat/sharp degrees is irregular for different scales. - refreshChordButton(this.theory, document.getElementById("toolboxChord" + buttonIndices[i]), keyChange, chord, rootPitch); - } - } - - for (var i = 0; i < this.chordOptions.length; i++) - this.chordOptions[i].style.border = "2px solid #ffffff"; - - this.chordOptions[this.currentChordKind].style.border = "2px solid #ff0000"; -} - - -Toolbox.prototype.refresh = function() -{ - document.getElementById("toolboxPlayButton").innerHTML = (this.playing ? "■ Stop" : "▶ Play"); - - if (this.playing) - return; - - this.refreshKeyPanel(); - this.refreshMeterPanel(); - this.refreshNotesPanel(); - this.refreshChordsPanel(); -} - - -Toolbox.prototype.insertNote = function(pitch) -{ - if (this.playing) - return; - - this.theory.playNoteSample(this.synth, pitch); - var note = new SongDataNote(this.editor.cursorTick, this.editor.songData.ticksPerWholeNote / 4, pitch); - this.editor.songData.addNote(note); - this.editor.cursorTick += this.editor.songData.ticksPerWholeNote / 4; - this.editor.showCursor = true; - this.editor.cursorZone = this.editor.CURSOR_ZONE_NOTES; - this.editor.unselectAll(); - this.editor.refreshRepresentation(); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.insertChord = function(chord, pitch) -{ - if (this.playing) - return; - - this.theory.playChordSample(this.synth, chord, pitch); - var songChord = new SongDataChord(this.editor.cursorTick, this.editor.songData.ticksPerWholeNote / 2, chord, pitch); - this.editor.songData.addChord(songChord); - this.editor.cursorTick += this.editor.songData.ticksPerWholeNote / 2; - this.editor.showCursor = true; - this.editor.cursorZone = this.editor.CURSOR_ZONE_CHORDS; - this.editor.unselectAll(); - this.editor.refreshRepresentation(); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.listenToKey = function() -{ - var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); - this.theory.playScaleSample(this.synth, keyChange.scale, keyChange.tonicPitch); -} - - -Toolbox.prototype.editKeyChange = function() -{ - if (this.playing) - return; - - var keyChange = this.editor.getKeyAtTick(this.editor.cursorTick); - if (keyChange == null) - return; - - var keyTonicSelect = document.getElementById("toolboxKeyTonicSelect"); - var keyScaleSelect = document.getElementById("toolboxKeyScaleSelect"); - - keyChange.scale = this.theory.scales[keyScaleSelect.selectedIndex]; - keyChange.tonicPitch = keyTonicSelect.selectedIndex; - - this.editor.refreshRepresentation(); - this.editor.selectKeyChange(keyChange); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.insertKeyChange = function() -{ - if (this.playing) - return; - - var keyChange = new SongDataKeyChange(this.editor.cursorTick, this.theory.scales[0], this.theory.C); - this.editor.songData.addKeyChange(keyChange); - this.editor.unselectAll(); - this.editor.refreshRepresentation(); - this.editor.selectKeyChange(keyChange); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.editMeterChange = function() -{ - if (this.playing) - return; - - var meterChange = this.editor.getMeterAtTick(this.editor.cursorTick); - if (meterChange == null) - return; - - var meterNumeratorSelect = document.getElementById("toolboxMeterNumeratorSelect"); - var meterDenominatorSelect = document.getElementById("toolboxMeterDenominatorSelect"); - - meterChange.numerator = meterNumeratorSelect.selectedIndex + 1; - meterChange.denominator = this.meterDenominators[meterDenominatorSelect.selectedIndex]; - - this.editor.refreshRepresentation(); - this.editor.selectMeterChange(meterChange); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.insertMeterChange = function() -{ - if (this.playing) - return; - - var meterChange = new SongDataMeterChange(this.editor.cursorTick, 4, 4); - this.editor.songData.addMeterChange(meterChange); - this.editor.unselectAll(); - this.editor.refreshRepresentation(); - this.editor.selectMeterChange(meterChange); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.togglePlay = function() -{ - this.playing = !this.playing; - this.editor.setInteractionEnabled(!this.playing); - - this.editor.cursorTick = Math.floor(this.editor.cursorTick / this.editor.tickSnap) * this.editor.tickSnap; - this.editor.showCursor = true; - this.editor.unselectAll(); - this.editor.clearHover(); - this.editor.refreshRepresentation(); - this.editor.refreshCanvas(); - this.refresh(); - - this.synth.stopAll(); - if (this.playing) - { - this.editor.cursorZone = this.editor.CURSOR_ZONE_ALL; - - var that = this; - this.playTimer = setInterval(function() { that.processPlayback(); }, 1000 / 60); - this.playTimerRefresh = setInterval(function() { that.processPlaybackRefresh(); }, 1000 / 15); - } - else - { - clearInterval(this.playTimer); - clearInterval(this.playTimerRefresh); - } -} - - -Toolbox.prototype.rewind = function() -{ - if (this.playing) - { - clearInterval(this.playTimer); - clearInterval(this.playTimerRefresh); - this.synth.stopAll(); - } - - this.playing = false; - this.editor.setInteractionEnabled(true); - - this.editor.cursorTick = 0; - this.editor.showCursor = true; - this.editor.unselectAll(); - this.editor.refreshRepresentation(); - this.editor.refreshCanvas(); - this.refresh(); -} - - -Toolbox.prototype.processPlayback = function() -{ - var bpm = this.editor.songData.beatsPerMinute; - var deltaTicks = bpm / 60 / 60 * (this.editor.songData.ticksPerWholeNote / 4); - - var lastCursorTick = this.editor.cursorTick; - this.editor.cursorTick += deltaTicks; - - // TODO: Use binary search. - for (var i = 0; i < this.editor.songData.notes.length; i++) - { - var note = this.editor.songData.notes[i]; - if (note.tick >= lastCursorTick && note.tick < this.editor.cursorTick) - { - this.synth.playNote(note.pitch, note.duration * 2, 1); - } - } - - for (var i = 0; i < this.editor.songData.chords.length; i++) - { - var chord = this.editor.songData.chords[i]; - if (chord.tick + chord.duration > lastCursorTick && chord.tick < this.editor.cursorTick && - !(chord.tick + chord.duration > lastCursorTick && chord.tick + chord.duration < this.editor.cursorTick)) - { - var halfTick = this.editor.songData.ticksPerWholeNote / 2; - var quarterTick = halfTick / 2; - var eighthTick = quarterTick / 2; - - var tick1 = (lastCursorTick - chord.tick); - var tick2 = (this.editor.cursorTick - chord.tick); - - if (Math.ceil(tick1 / halfTick) != Math.ceil(tick2 / halfTick)) - { - for (var j = 0; j < chord.chord.pitches.length; j++) - this.synth.playNote((chord.chord.pitches[j] + chord.rootPitch) % 12 + 60, quarterTick, 0.75); - } - else if (Math.ceil(tick1 / quarterTick) != Math.ceil(tick2 / quarterTick)) - { - for (var j = 1; j < chord.chord.pitches.length; j++) - this.synth.playNote((chord.chord.pitches[j] + chord.rootPitch) % 12 + 60, eighthTick, 0.5); - } - else if (Math.ceil(tick1 / eighthTick) != Math.ceil(tick2 / eighthTick)) - { - this.synth.playNote((chord.chord.pitches[0] + chord.rootPitch) % 12 + 60, eighthTick, 0.45); - } - } - } -} - - -Toolbox.prototype.processPlaybackRefresh = function() -{ - this.editor.unselectAll(); - - // TODO: Use binary search. - for (var i = 0; i < this.editor.songData.notes.length; i++) - { - var note = this.editor.songData.notes[i]; - if (note.tick + note.duration >= this.editor.cursorTick && note.tick < this.editor.cursorTick) - this.editor.noteSelections[i] = true; - } - - for (var i = 0; i < this.editor.songData.chords.length; i++) - { - var chord = this.editor.songData.chords[i]; - if (chord.tick + chord.duration > this.editor.cursorTick && chord.tick < this.editor.cursorTick) - this.editor.chordSelections[i] = true; - } - - this.editor.refreshCanvas(); -} \ No newline at end of file diff --git a/src/util/callback.js b/src/util/callback.js new file mode 100644 index 0000000..3abcad6 --- /dev/null +++ b/src/util/callback.js @@ -0,0 +1,27 @@ +function Callback() +{ + this.callbacks = []; +} + + +Callback.prototype.add = function(callback) +{ + this.callbacks.push(callback); +} + + +Callback.prototype.remove = function(callback) +{ + var index = this.callbacks.indexOf(callback); + + if (index > -1) { + this.callbacks.splice(index, 1); + } +} + + +Callback.prototype.call = function(callbackWithArgs) +{ + for (var i = 0; i < this.callbacks.length; i++) + callbackWithArgs(this.callbacks[i]); +} \ No newline at end of file diff --git a/src/util/dom.js b/src/util/dom.js new file mode 100644 index 0000000..179e6be --- /dev/null +++ b/src/util/dom.js @@ -0,0 +1,9 @@ +function mouseToClient(elem, ev) +{ + var rect = elem.getBoundingClientRect(); + + return { + x: ev.clientX - rect.left, + y: ev.clientY - rect.top + }; +} \ No newline at end of file diff --git a/src/util/map_by_time.js b/src/util/map_by_time.js new file mode 100644 index 0000000..4743071 --- /dev/null +++ b/src/util/map_by_time.js @@ -0,0 +1,60 @@ +// TODO: Optimize with better data structures. +function MapByTime() +{ + this.items = []; +} + + +MapByTime.prototype.add = function(id, item) +{ + this.items[id] = item; +} + + +MapByTime.prototype.remove = function(id) +{ + this.items[id] = null; +} + + +MapByTime.prototype.get = function(id) +{ + return this.items[id]; +} + + +MapByTime.prototype.enumerateAll = function(callback) +{ + for (var i = 0; i < this.items.length; i++) + callback(i, this.items[i]); +} + + +MapByTime.prototype.enumerateOverlappingRange = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (timeRange.overlapsTime(this.items[i].time)) + callback(i, this.items[i]); + } +} + + +MapByTime.prototype.getActiveAtTime = function(time) +{ + var index = -1; + var minTimeSoFar = -1; + + for (var i = 0; i < this.items.length; i++) + { + var itemTime = this.items[i].time; + + if (minTimeSoFar == -1 || (itemTime < minTimeSoFar && itemTime >= time)) + { + index = i; + minTimeSoFar = itemTime; + } + } + + return index; +} \ No newline at end of file diff --git a/src/util/map_by_timerange.js b/src/util/map_by_timerange.js new file mode 100644 index 0000000..d52c5eb --- /dev/null +++ b/src/util/map_by_timerange.js @@ -0,0 +1,60 @@ +// TODO: Optimize with better data structures. +function MapByTimeRange() +{ + this.items = []; +} + + +MapByTimeRange.prototype.add = function(id, item) +{ + this.items[id] = item; +} + + +MapByTimeRange.prototype.remove = function(id) +{ + this.items[id] = null; +} + + +MapByTimeRange.prototype.get = function(id) +{ + return this.items[id]; +} + + +MapByTimeRange.prototype.enumerateAll = function(callback) +{ + for (var i = 0; i < this.items.length; i++) + callback(i, this.items[i]); +} + + +MapByTimeRange.prototype.enumerateOverlappingTime = function(time, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i].timeRange.overlapsTime(time)) + callback(i, this.items[i]); + } +} + + +MapByTimeRange.prototype.enumerateOverlappingRange = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (timeRange.overlapsRange(this.items[i].timeRange)) + callback(i, this.items[i]); + } +} + + +MapByTimeRange.prototype.enumerateOverlappingRangeOrSelected = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i].selected || timeRange.overlapsRange(this.items[i].timeRange)) + callback(i, this.items[i]); + } +} \ No newline at end of file diff --git a/src/util/pitch.js b/src/util/pitch.js new file mode 100644 index 0000000..5c5c803 --- /dev/null +++ b/src/util/pitch.js @@ -0,0 +1,10 @@ +function Pitch(midiPitch) +{ + this.midiPitch = midiPitch; +} + + +Pitch.prototype.clone = function() +{ + return new Pitch(this.midiPitch); +} \ No newline at end of file diff --git a/src/util/timerange.js b/src/util/timerange.js new file mode 100644 index 0000000..b635d41 --- /dev/null +++ b/src/util/timerange.js @@ -0,0 +1,36 @@ +function TimeRange(start, end) +{ + this.start = start; + this.end = end; +} + + +TimeRange.prototype.clone = function() +{ + return new TimeRange(this.start, this.end); +} + + +TimeRange.prototype.merge = function(other) +{ + this.start = Math.min(this.start, other.start) + this.end = Math.max(this.end, other.end); +} + + +TimeRange.prototype.duration = function() +{ + return this.end - this.start; +} + + +TimeRange.prototype.overlapsTime = function(time) +{ + return time >= this.start && time < this.end; +} + + +TimeRange.prototype.overlapsRange = function(other) +{ + return this.start < other.end && this.end >= other.start; +} \ No newline at end of file diff --git a/toolbox.css b/toolbox.css deleted file mode 100644 index cc21c2d..0000000 --- a/toolbox.css +++ /dev/null @@ -1,111 +0,0 @@ -.toolboxHeader -{ - font-family: tahoma; - font-size: 14px; - font-weight: bold; -} - -.toolboxLabel -{ - font-family: tahoma; - font-size: 12px; -} - -.toolboxButton -{ - margin: 2px; - border: 2px solid #aaaaaa; - background: #ffffff; - font-family: tahoma; - font-size: 12px; - outline: none; -} - -.toolboxButton:hover -{ - border-color: #cccccc; - cursor: pointer; -} - -.toolboxButton:active -{ - border-color: #aaaaaa; - background: #eeeeee; -} - -.toolboxNoteButton -{ - width: 20px; - height: 30px; - margin: 2px; - padding: 0px; - border: none; - text-align: center; - font-family: tahoma; - font-size: 12px; - outline: none; -} - -.toolboxNoteButton:hover -{ - cursor: pointer; - border: 1px solid #ffffff; -} - -.toolboxNoteButton:active -{ - border: 2px solid #ffffff; -} - -.toolboxChordButton -{ - width: 50px; - height: 38px; - margin: 2px; - padding: 0px; - border-style: solid; - border-width: 3px 0 3px 0; - background: #eeeeee; - text-align: center; - font-family: tahoma; - font-size: 18px; - outline: none; - overflow: hidden; -} - -.toolboxChordButton:hover -{ - cursor: pointer; - background: #f8f8f8; -} - -.toolboxChordButton:active -{ - background: #f0f0f0; -} - -.toolboxChordKindButton -{ - border: 2px solid #ffffff; - background: #ffffff; - font-family: tahoma; - font-size: 12px; - outline: none; - min-width: 30px; -} - -.toolboxChordKindButton:hover -{ - background: #eeeeee; - cursor: pointer; -} - -.toolboxChordKindButton:active -{ - background: #cccccc; -} - -.toolboxPalette -{ - overflow-y: scroll; -} \ No newline at end of file From 3b38eebf61f6e8545d93720d26afd998a861c486 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Fri, 19 Aug 2016 08:16:28 -0300 Subject: [PATCH 29/46] add element modification by mouse dragging --- index.html | 23 +++---- src/song/song.js | 35 +++++++--- src/timeline/timeline.js | 125 ++++++++++++++++++++++++++--------- src/timeline/track_notes.js | 127 ++++++++++++++++++++++++------------ src/util/math.js | 4 ++ 5 files changed, 225 insertions(+), 89 deletions(-) create mode 100644 src/util/math.js diff --git a/index.html b/index.html index d7e4f84..aa5cef9 100644 --- a/index.html +++ b/index.html @@ -13,17 +13,18 @@
- - - - - - - - - + + + + + + + + + - - + + + \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js index c6b2bf6..a346539 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -6,16 +6,17 @@ function Song() this.MIN_VALID_MIDI_PITCH = 3 * 12; this.MAX_VALID_MIDI_PITCH = 8 * 12; - this.notesById = []; - this.metersById = []; + this.notesById = []; + this.notesModified = []; + this.metersById = []; - this.eventNoteAdded = new Callback(); - this.eventNoteChanged = new Callback(); - this.eventNoteRemoved = new Callback(); + this.eventNoteAdded = new Callback(); + this.eventNoteModified = new Callback(); + this.eventNoteRemoved = new Callback(); - this.eventMeterAdded = new Callback(); - this.eventMeterChanged = new Callback(); - this.eventMeterRemoved = new Callback(); + this.eventMeterAdded = new Callback(); + this.eventMeterModified = new Callback(); + this.eventMeterRemoved = new Callback(); this.sanitize(); } @@ -59,6 +60,17 @@ Song.prototype.raiseAllRemoved = function() } +Song.prototype.applyModifications = function() +{ + var that = this; + + for (var i = 0; i < this.notesModified.length; i++) + this.eventNoteModified.call(function (fn) { fn(that.notesModified[i]); }); + + this.notesModified = []; +} + + Song.prototype.noteAdd = function(note) { var id = this.notesById.length; @@ -76,6 +88,13 @@ Song.prototype.noteRemove = function(id) } +Song.prototype.noteModify = function(id, note) +{ + this.notesById[id] = note; + this.notesModified.push(id); +} + + Song.prototype.noteGet = function(id) { return this.notesById[id]; diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 618c0df..6fb96cc 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -21,11 +21,11 @@ function Timeline(canvas) this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; - this.mouseDown = false; - this.mouseDownPos = null; - this.mouseAction = this.INTERACT_NONE; - this.mouseMoveTime = 0; - this.mouseMovePitch = 0; + this.mouseDown = false; + this.mouseDownPos = null; + this.mouseAction = this.INTERACT_NONE; + this.mouseMoveDeltaTime = 0; + this.mouseMovePitch = 0; this.hoverElement = null; this.hoverRegion = null; @@ -34,14 +34,15 @@ function Timeline(canvas) // Set up song and song events. this.song = null; - this.eventNoteAdded = new Callback(); - this.eventNoteChanged = new Callback(); - this.eventNoteRemoved = new Callback(); + this.eventNoteAdded = new Callback(); + this.eventNoteModified = new Callback(); + this.eventNoteRemoved = new Callback(); // Set up display metrics. - this.scrollTime = 0; + this.scrollTime = 0; + this.timeSnap = 960 / 16; this.timeToPixelsScaling = 100 / 960; - this.noteHeight = 10; + this.noteHeight = 5; this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; @@ -65,9 +66,9 @@ Timeline.prototype.setSong = function(song) if (this.song != null) { var that = this; - this.song.eventNoteAdded .add(function (id) { that.eventNoteAdded .call(function (fn) { fn(id); }); }); - this.song.eventNoteChanged.add(function (id) { that.eventNoteChanged.call(function (fn) { fn(id); }); }); - this.song.eventNoteRemoved.add(function (id) { that.eventNoteRemoved.call(function (fn) { fn(id); }); }); + this.song.eventNoteAdded .add(function (id) { that.eventNoteAdded .call(function (fn) { fn(id); }); }); + this.song.eventNoteModified.add(function (id) { that.eventNoteModified.call(function (fn) { fn(id); }); }); + this.song.eventNoteRemoved .add(function (id) { that.eventNoteRemoved .call(function (fn) { fn(id); }); }); this.song.raiseAllAdded(); this.markDirtyAll(); } @@ -83,22 +84,28 @@ Timeline.prototype.handleMouseDown = function(ev) var ctrl = ev.ctrlKey; var mousePos = mouseToClient(this.canvas, ev); + // Handle multi-selection with Ctrl. if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) this.unselectAll(); if (this.hoverElement != null) { - this.select(this.hoverElement); + // Handle selection of element under mouse. + if (!this.hoverElement.selected) + this.select(this.hoverElement); + this.unselectAllOfDifferentKind(this.hoverElement.interactKind); this.mouseAction = this.hoverElement.interactKind; + this.markDirtyAllSelectedElements(0); } else this.mouseAction = this.INTERACT_NONE; - this.mouseDown = true; - this.mouseDownPos = mousePos; - this.mouseMoveTime = 0; + this.mouseDown = true; + this.mouseDownPos = mousePos; + this.mouseMoveDeltaTime = 0; + this.mouseMoveDeltaPitch = 0; this.redraw(); } @@ -110,40 +117,75 @@ Timeline.prototype.handleMouseMove = function(ev) ev.preventDefault(); var mousePos = mouseToClient(this.canvas, ev); - var mouseTime = mousePos.x / this.timeToPixelsScaling; + var mouseTime = snap(mousePos.x / this.timeToPixelsScaling, this.timeSnap); + // Handle dragging with the mouse. if (this.mouseDown) { - var mouseTimeDelta = (mousePos.x - this.mouseDownPos.x) / this.timeToPixelsScaling; + var mouseTimeDelta = (mousePos.x - this.mouseDownPos.x) / this.timeToPixelsScaling; + var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); if (this.selectedElements.length != 0) { + // Handle time displacement. if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) { - var allEncompasingTimeRange = this.selectedElements[0].timeRange.clone(); + // Get merged time ranges of all selected elements. var allEncompasingInteractTimeRange = this.selectedElements[0].interactTimeRange.clone(); for (var i = 1; i < this.selectedElements.length; i++) - { - allEncompasingTimeRange .merge(this.selectedElements[i].timeRange); allEncompasingInteractTimeRange.merge(this.selectedElements[i].interactTimeRange); - } - this.markDirty( - allEncompasingTimeRange.start + this.mouseMoveTime, - allEncompasingTimeRange.end + this.mouseMoveTime); + // Mark elements' previous positions as dirty, + // to redraw over when they move away. + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - this.mouseMoveTime = + // Calculate displacement, + // ensuring that elements cannot fall out of bounds. + this.mouseMoveDeltaTime = Math.max(-allEncompasingInteractTimeRange.start, Math.min(this.song.length - allEncompasingInteractTimeRange.end, mouseTimeDelta)); - this.markDirty( - allEncompasingTimeRange.start + this.mouseMoveTime, - allEncompasingTimeRange.end + this.mouseMoveTime); + this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they move to. + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); + } + // If not displacing time, mark elements as dirty in their current positions. + else + this.markDirtyAllSelectedElements(0); + + // Handle pitch displacement. + if ((this.mouseAction & this.INTERACT_MOVE_PITCH) != 0) + { + // Get the pitch range of all selected elements. + var pitchMin = this.selectedElements[0].interactPitch.clone(); + var pitchMax = this.selectedElements[0].interactPitch.clone(); + + for (var i = 1; i < this.selectedElements.length; i++) + { + var pitch = this.selectedElements[i].interactPitch.midiPitch; + + if (pitch < pitchMin.midiPitch) + pitchMin = new Pitch(pitch); + + if (pitch > pitchMax.midiPitch) + pitchMax = new Pitch(pitch); + } + + // Calculate displacement, + // ensuring that pitches cannot fall out of bounds. + this.mouseMoveDeltaPitch = + Math.max(this.song.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, + Math.min(this.song.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, + mousePitchDelta)); } } } + + // If mouse is not down, just handle hovering. else { if (this.hoverElement != null) @@ -203,6 +245,18 @@ Timeline.prototype.handleMouseUp = function(ev) var mousePos = mouseToClient(this.canvas, ev); + // Handle releasing the mouse after dragging. + if (this.mouseDown) + { + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); + + for (var i = 0; i < this.selectedElements.length; i++) + this.selectedElements[i].modify(this.selectedElements[i]); + + this.song.applyModifications(); + this.markDirtyAllSelectedElements(0); + } + this.mouseDown = false; this.mouseAction = this.INTERACT_NONE; this.redraw(); @@ -232,6 +286,17 @@ Timeline.prototype.markDirtyElement = function(elem) } +Timeline.prototype.markDirtyAllSelectedElements = function(timeDelta) +{ + for (var i = 0; i < this.selectedElements.length; i++) + { + this.markDirty( + this.selectedElements[i].timeRange.start + timeDelta, + this.selectedElements[i].timeRange.end + timeDelta); + } +} + + Timeline.prototype.unselectAll = function() { for (var i = 0; i < this.selectedElements.length; i++) diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 2ca15e5..f1ffef1 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -3,8 +3,9 @@ function TrackNotes(timeline) this.timeline = timeline; var that = this; - this.timeline.eventNoteAdded .add(function (id) { that.onNoteAdded(id); }); - this.timeline.eventNoteRemoved.add(function (id) { that.onNoteRemoved(id); }); + this.timeline.eventNoteAdded .add(function (id) { that.onNoteAdded (id); }); + this.timeline.eventNoteModified.add(function (id) { that.onNoteModified(id); }); + this.timeline.eventNoteRemoved .add(function (id) { that.onNoteRemoved (id); }); this.y = 0; this.height = 0; @@ -17,35 +18,59 @@ function TrackNotes(timeline) TrackNotes.prototype.onNoteAdded = function(id) { - var toPixels = this.timeline.timeToPixelsScaling; - var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; - - var note = this.timeline.song.noteGet(id); + var that = this; var elem = { id: id, selected: false, - timeRange: note.timeRange.clone(), + timeRange: null, + + modify: function(elem) { that.elementModify(elem); }, regions: [], interactKind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, - interactTimeRange: note.timeRange.clone(), - interactPitch: note.pitch.clone() + interactTimeRange: null, + interactPitch: null }; - this.refreshElementRegions(elem); + this.elementRefresh(elem); this.elements.add(id, elem); } +TrackNotes.prototype.onNoteModified = function(id) +{ + var elem = this.elements.get(id); + this.elementRefresh(elem); +} + + TrackNotes.prototype.onNoteRemoved = function(id) { this.elements.remove(id); } -TrackNotes.prototype.refreshElementRegions = function(elem) +TrackNotes.prototype.elementModify = function(elem) +{ + var note = this.timeline.song.noteGet(elem.id); + + var start = note.timeRange.start; + var duration = note.timeRange.duration(); + var pitch = note.pitch.midiPitch; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + start += this.timeline.mouseMoveDeltaTime; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) + pitch += this.timeline.mouseMoveDeltaPitch; + + this.timeline.song.noteModify(elem.id, + new Note(new TimeRange(start, start + duration), new Pitch(pitch))); +} + + +TrackNotes.prototype.elementRefresh = function(elem) { var toPixels = this.timeline.timeToPixelsScaling; var noteHeight = this.timeline.noteHeight; @@ -53,6 +78,10 @@ TrackNotes.prototype.refreshElementRegions = function(elem) var note = this.timeline.song.noteGet(elem.id); + elem.timeRange = note.timeRange.clone(); + elem.interactTimeRange = note.timeRange.clone(); + elem.interactPitch = note.pitch.clone(); + elem.regions = [ { kind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, @@ -71,7 +100,7 @@ TrackNotes.prototype.relayout = function() this.elements.enumerateAll(function (index, elem) { - that.refreshElementRegions(elem); + that.elementRefresh(elem); }); } @@ -110,37 +139,55 @@ TrackNotes.prototype.redraw = function(time1, time2) // Draw notes. this.elements.enumerateOverlappingRangeOrSelected(new TimeRange(time1, time2), function (index, elem) { - var note = that.timeline.song.noteGet(elem.id); - - var start = note.timeRange.start; - var duration = note.timeRange.duration(); - if (elem.selected && (that.timeline.mouseAction & that.timeline.INTERACT_MOVE_TIME) != 0) - start += that.timeline.mouseMoveTime; - - ctx.globalAlpha = 1; - if (elem == that.timeline.hoverElement) - ctx.fillStyle = "#ff8888"; - else - ctx.fillStyle = "#ff0000"; - - var x = 0.5 + Math.floor(start * toPixels); - var y = 0.5 + that.height - noteHeight * (note.pitch.midiPitch - minPitch); - var w = Math.floor(duration * toPixels); - - ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); - - if (elem.selected) - { - ctx.fillStyle = "#ffffff" - ctx.globalAlpha = 0.5; - - ctx.fillRect(x, y - noteHeight + 2, w, noteHeight - 1 - 4); - } + that.drawNote(elem); }); - + // Draw borders. ctx.strokeStyle = "#000000"; ctx.strokeRect(0, 0, this.timeline.song.length * toPixels, this.height); ctx.restore(); +} + + +TrackNotes.prototype.drawNote = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + var note = this.timeline.song.noteGet(elem.id); + + var start = note.timeRange.start; + var duration = note.timeRange.duration(); + var pitch = note.pitch.midiPitch; + + if (elem.selected) + { + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + start += this.timeline.mouseMoveDeltaTime; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) + pitch += this.timeline.mouseMoveDeltaPitch; + } + + ctx.globalAlpha = 1; + if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) + ctx.fillStyle = "#ff8888"; + else + ctx.fillStyle = "#ff0000"; + + var x = 0.5 + Math.floor(start * toPixels); + var y = 0.5 + this.height - noteHeight * (pitch - minPitch); + var w = Math.floor(duration * toPixels); + + ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + + if (elem.selected) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.5; + + ctx.fillRect(x, y - noteHeight + 1, w, noteHeight - 1 - 2); + } } \ No newline at end of file diff --git a/src/util/math.js b/src/util/math.js new file mode 100644 index 0000000..a044a52 --- /dev/null +++ b/src/util/math.js @@ -0,0 +1,4 @@ +function snap(x, step) +{ + return Math.round(x / step) * step; +} \ No newline at end of file From 6c1ea6d257c4008994a6e17a60790a0dd34ff5d7 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Fri, 19 Aug 2016 19:26:59 -0300 Subject: [PATCH 30/46] add note clipping; add note track vertical scroll --- src/song/note.js | 6 +++ src/song/song.js | 54 ++++++++++++++++++++++---- src/timeline/timeline.js | 74 +++++++++++++++++++++++++++--------- src/timeline/track_notes.js | 71 +++++++++++++++++++++++++++------- src/util/map_by_time.js | 17 +++++++++ src/util/map_by_timerange.js | 20 ++++++++++ src/util/timerange.js | 21 +++++++++- 7 files changed, 222 insertions(+), 41 deletions(-) diff --git a/src/song/note.js b/src/song/note.js index 746de7b..cef3de9 100644 --- a/src/song/note.js +++ b/src/song/note.js @@ -2,4 +2,10 @@ function Note(timeRange, pitch) { this.timeRange = timeRange; this.pitch = pitch; +} + + +Note.prototype.clone = function() +{ + return new Note(this.timeRange.clone(), this.pitch.clone()); } \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js index a346539..d7586be 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -62,21 +62,61 @@ Song.prototype.raiseAllRemoved = function() Song.prototype.applyModifications = function() { - var that = this; - for (var i = 0; i < this.notesModified.length; i++) - this.eventNoteModified.call(function (fn) { fn(that.notesModified[i]); }); + { + var modif = this.notesModified[i]; + this._noteSet(modif.id, modif.note, true); + } this.notesModified = []; } +Song.prototype._clipNotes = function(note) +{ + // Check for overlapping notes and clip them. + for (var i = 0; i < this.notesById.length; i++) + { + var otherNote = this.notesById[i]; + if (otherNote == null) + continue; + + if (otherNote.pitch.midiPitch != note.pitch.midiPitch) + continue; + + if (!otherNote.timeRange.overlapsRange(note.timeRange)) + continue; + + this.noteRemove(i); + + var parts = otherNote.timeRange.getClippedParts(note.timeRange); + for (var p = 0; p < parts.length; p++) + { + var clippedNote = otherNote.clone(); + clippedNote.timeRange = parts[p]; + this.noteAdd(clippedNote); + } + } +} + + +Song.prototype._noteSet = function(id, note, asModification) +{ + this._clipNotes(note); + this.notesById[id] = note; + + if (asModification) + this.eventNoteModified.call(function (fn) { fn(id); }); + else + this.eventNoteAdded .call(function (fn) { fn(id); }); +} + + Song.prototype.noteAdd = function(note) { var id = this.notesById.length; - this.notesById[id] = note; + this._noteSet(id, note, false); - this.eventNoteAdded.call(function (fn) { fn(id); }); return id; } @@ -90,8 +130,8 @@ Song.prototype.noteRemove = function(id) Song.prototype.noteModify = function(id, note) { - this.notesById[id] = note; - this.notesModified.push(id); + this.notesById[id] = null; + this.notesModified.push({ id: id, note: note }); } diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 6fb96cc..db963ae 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -16,16 +16,18 @@ function Timeline(canvas) this.canvasHeight = 0; // Set up mouse/keyboard interaction. - this.canvas.onmousemove = function(ev) { that.handleMouseMove(ev); }; this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; - this.canvas.onmouseup = function(ev) { that.handleMouseUp(ev); }; + window.onmousemove = function(ev) { that.handleMouseMove(ev); }; + window.onmouseup = function(ev) { that.handleMouseUp(ev); }; //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; this.mouseDown = false; this.mouseDownPos = null; + this.mouseDownTrack = null; this.mouseAction = this.INTERACT_NONE; this.mouseMoveDeltaTime = 0; this.mouseMovePitch = 0; + this.mouseMoveScrollY = 0; this.hoverElement = null; this.hoverRegion = null; @@ -42,7 +44,7 @@ function Timeline(canvas) this.scrollTime = 0; this.timeSnap = 960 / 16; this.timeToPixelsScaling = 100 / 960; - this.noteHeight = 5; + this.noteHeight = 10; this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; @@ -102,10 +104,14 @@ Timeline.prototype.handleMouseDown = function(ev) else this.mouseAction = this.INTERACT_NONE; + var trackIndex = this.getTrackIndexAtY(mousePos.y); + this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); + this.mouseDown = true; this.mouseDownPos = mousePos; this.mouseMoveDeltaTime = 0; this.mouseMoveDeltaPitch = 0; + this.mouseMoveScrollY = 0; this.redraw(); } @@ -125,6 +131,13 @@ Timeline.prototype.handleMouseMove = function(ev) var mouseTimeDelta = (mousePos.x - this.mouseDownPos.x) / this.timeToPixelsScaling; var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); + // Handle scrolling. + if (this.mouseAction == this.INTERACT_NONE) + { + this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); + this.markDirtyAll(); + } + if (this.selectedElements.length != 0) { // Handle time displacement. @@ -194,12 +207,10 @@ Timeline.prototype.handleMouseMove = function(ev) this.hoverElement = null; this.hoverRegion = null; - for (var i = 0; i < this.tracks.length; i++) + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) { - var track = this.tracks[i]; - - if (mousePos.y < track.y || mousePos.y >= track.y + track.height) - continue; + var track = this.tracks[trackIndex]; track.elements.enumerateOverlappingTime(mouseTime, function (index, elem) { @@ -209,8 +220,8 @@ Timeline.prototype.handleMouseMove = function(ev) if (mousePos.x >= region.x && mousePos.x <= region.x + region.width && - mousePos.y >= region.y + track.y && - mousePos.y <= region.y + region.height + track.y) + mousePos.y >= region.y + track.y + track.scrollY && + mousePos.y <= region.y + region.height + track.y + track.scrollY) { that.hoverElement = elem; that.hoverRegion = region; @@ -248,17 +259,26 @@ Timeline.prototype.handleMouseUp = function(ev) // Handle releasing the mouse after dragging. if (this.mouseDown) { - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - - for (var i = 0; i < this.selectedElements.length; i++) - this.selectedElements[i].modify(this.selectedElements[i]); - - this.song.applyModifications(); - this.markDirtyAllSelectedElements(0); + if (this.mouseAction == this.INTERACT_NONE) + { + if (this.mouseDownTrack != null) + this.mouseDownTrack.handleScroll(); + } + else + { + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); + + for (var i = 0; i < this.selectedElements.length; i++) + this.selectedElements[i].modify(this.selectedElements[i]); + + this.song.applyModifications(); + this.markDirtyAllSelectedElements(0); + } } - this.mouseDown = false; - this.mouseAction = this.INTERACT_NONE; + this.mouseDown = false; + this.mouseAction = this.INTERACT_NONE; + this.mouseDownTrack = null; this.redraw(); } @@ -331,6 +351,20 @@ Timeline.prototype.select = function(elem) } +Timeline.prototype.getTrackIndexAtY = function(y) +{ + for (var i = 0; i < this.tracks.length; i++) + { + var track = this.tracks[i]; + + if (y >= track.y && y < track.y + track.height) + return i; + } + + return -1; +} + + Timeline.prototype.relayout = function() { this.canvasWidth = parseFloat(this.canvas.width); @@ -346,11 +380,13 @@ Timeline.prototype.relayout = function() Timeline.prototype.redraw = function() { + // Return early if nothing dirty. if (this.redrawDirtyTimeMin == -1 || this.redrawDirtyTimeMax == -1) return; this.ctx.save(); + // Restrict drawing to dirty region. this.ctx.beginPath(); this.ctx.rect( this.redrawDirtyTimeMin * this.timeToPixelsScaling + 1, diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index f1ffef1..bc91527 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -10,7 +10,7 @@ function TrackNotes(timeline) this.y = 0; this.height = 0; - this.scrollMidiPitch = 0; + this.scrollY = 0; this.elements = new MapByTimeRange(); } @@ -42,15 +42,24 @@ TrackNotes.prototype.onNoteModified = function(id) { var elem = this.elements.get(id); this.elementRefresh(elem); + this.elements.refresh(id); } TrackNotes.prototype.onNoteRemoved = function(id) { + this.timeline.markDirtyElement(this.elements.get(id)); this.elements.remove(id); } +TrackNotes.prototype.handleScroll = function(elem) +{ + this.scrollY = this.getModifiedScrollY(); + this.timeline.markDirtyAll(); +} + + TrackNotes.prototype.elementModify = function(elem) { var note = this.timeline.song.noteGet(elem.id); @@ -107,12 +116,13 @@ TrackNotes.prototype.relayout = function() TrackNotes.prototype.redraw = function(time1, time2) { - var that = this; - var ctx = this.timeline.ctx; - var toPixels = this.timeline.timeToPixelsScaling; - var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; - var maxPitch = this.timeline.song.MAX_VALID_MIDI_PITCH; + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.song.MAX_VALID_MIDI_PITCH; + var scrollY = this.getModifiedScrollY(); var xMin = time1 * toPixels; var xMax = Math.min(time2, this.timeline.song.length) * toPixels; @@ -125,6 +135,9 @@ TrackNotes.prototype.redraw = function(time1, time2) ctx.translate(0.5, 0.5); + ctx.save(); + ctx.translate(0, scrollY); + // Draw pitch rows. ctx.fillStyle = "#e4e4e4"; for (var i = minPitch; i <= maxPitch; i++) @@ -142,6 +155,8 @@ TrackNotes.prototype.redraw = function(time1, time2) that.drawNote(elem); }); + ctx.restore(); + // Draw borders. ctx.strokeStyle = "#000000"; ctx.strokeRect(0, 0, this.timeline.song.length * toPixels, this.height); @@ -150,13 +165,36 @@ TrackNotes.prototype.redraw = function(time1, time2) } +TrackNotes.prototype.getModifiedScrollY = function() +{ + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.song.MAX_VALID_MIDI_PITCH; + + var scrollY = this.scrollY; + + if (this.timeline.mouseDownTrack == this && + this.timeline.mouseAction == this.timeline.INTERACT_NONE) + { + scrollY += this.timeline.mouseMoveScrollY; + scrollY = + Math.max(0, + Math.min((maxPitch - minPitch) * noteHeight - this.height, + scrollY)); + } + + return scrollY; +} + + TrackNotes.prototype.drawNote = function(elem) { - var ctx = this.timeline.ctx; - var toPixels = this.timeline.timeToPixelsScaling; - var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; - var note = this.timeline.song.noteGet(elem.id); + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + var note = this.timeline.song.noteGet(elem.id); + var scrollY = this.getModifiedScrollY(); var start = note.timeRange.start; var duration = note.timeRange.duration(); @@ -179,7 +217,12 @@ TrackNotes.prototype.drawNote = function(elem) var x = 0.5 + Math.floor(start * toPixels); var y = 0.5 + this.height - noteHeight * (pitch - minPitch); - var w = Math.floor(duration * toPixels); + var w = Math.floor(duration * toPixels - 1); + + y = + Math.max(-scrollY + 3, + Math.min(this.height - scrollY + noteHeight - 2, + y)); ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); @@ -188,6 +231,6 @@ TrackNotes.prototype.drawNote = function(elem) ctx.fillStyle = "#ffffff" ctx.globalAlpha = 0.5; - ctx.fillRect(x, y - noteHeight + 1, w, noteHeight - 1 - 2); + ctx.fillRect(x, y - noteHeight + 2, w, noteHeight - 1 - 4); } } \ No newline at end of file diff --git a/src/util/map_by_time.js b/src/util/map_by_time.js index 4743071..fbba126 100644 --- a/src/util/map_by_time.js +++ b/src/util/map_by_time.js @@ -23,10 +23,21 @@ MapByTime.prototype.get = function(id) } +MapByTime.prototype.refresh = function(id) +{ + // Do nothing for this unoptimized implementation. +} + + MapByTime.prototype.enumerateAll = function(callback) { for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + callback(i, this.items[i]); + } } @@ -34,6 +45,9 @@ MapByTime.prototype.enumerateOverlappingRange = function(timeRange, callback) { for (var i = 0; i < this.items.length; i++) { + if (this.items[i] == null) + continue; + if (timeRange.overlapsTime(this.items[i].time)) callback(i, this.items[i]); } @@ -47,6 +61,9 @@ MapByTime.prototype.getActiveAtTime = function(time) for (var i = 0; i < this.items.length; i++) { + if (this.items[i] == null) + continue; + var itemTime = this.items[i].time; if (minTimeSoFar == -1 || (itemTime < minTimeSoFar && itemTime >= time)) diff --git a/src/util/map_by_timerange.js b/src/util/map_by_timerange.js index d52c5eb..c30064b 100644 --- a/src/util/map_by_timerange.js +++ b/src/util/map_by_timerange.js @@ -23,10 +23,21 @@ MapByTimeRange.prototype.get = function(id) } +MapByTimeRange.prototype.refresh = function(id) +{ + // Do nothing for this unoptimized implementation. +} + + MapByTimeRange.prototype.enumerateAll = function(callback) { for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + callback(i, this.items[i]); + } } @@ -34,6 +45,9 @@ MapByTimeRange.prototype.enumerateOverlappingTime = function(time, callback) { for (var i = 0; i < this.items.length; i++) { + if (this.items[i] == null) + continue; + if (this.items[i].timeRange.overlapsTime(time)) callback(i, this.items[i]); } @@ -44,6 +58,9 @@ MapByTimeRange.prototype.enumerateOverlappingRange = function(timeRange, callbac { for (var i = 0; i < this.items.length; i++) { + if (this.items[i] == null) + continue; + if (timeRange.overlapsRange(this.items[i].timeRange)) callback(i, this.items[i]); } @@ -54,6 +71,9 @@ MapByTimeRange.prototype.enumerateOverlappingRangeOrSelected = function(timeRang { for (var i = 0; i < this.items.length; i++) { + if (this.items[i] == null) + continue; + if (this.items[i].selected || timeRange.overlapsRange(this.items[i].timeRange)) callback(i, this.items[i]); } diff --git a/src/util/timerange.js b/src/util/timerange.js index b635d41..0202b6f 100644 --- a/src/util/timerange.js +++ b/src/util/timerange.js @@ -18,6 +18,25 @@ TimeRange.prototype.merge = function(other) } +TimeRange.prototype.getClippedParts = function(clipRange) +{ + var parts = []; + + if (!this.overlapsRange(clipRange)) + parts.push(this.clone()); + else + { + if (clipRange.start > this.start) + parts.push(new TimeRange(this.start, clipRange.start)); + + if (clipRange.end < this.end) + parts.push(new TimeRange(clipRange.end, this.end)); + } + + return parts; +} + + TimeRange.prototype.duration = function() { return this.end - this.start; @@ -32,5 +51,5 @@ TimeRange.prototype.overlapsTime = function(time) TimeRange.prototype.overlapsRange = function(other) { - return this.start < other.end && this.end >= other.start; + return this.start < other.end && this.end > other.start; } \ No newline at end of file From 000728d27d78272153ffb1edebb6b824d1189783 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 20 Aug 2016 11:28:59 -0300 Subject: [PATCH 31/46] simplify song handling by having tracks keep the contents internally --- index.html | 28 ++-- src/song/song.js | 148 +------------------- src/timeline/element.js | 16 +++ src/timeline/timeline.js | 52 ++++--- src/timeline/track.js | 16 +++ src/timeline/track_notes.js | 237 ++++++++++++++++++++------------ src/util/{dom.js => browser.js} | 0 src/util/list_by_time.js | 78 +++++++++++ src/util/list_by_timerange.js | 67 +++++++++ src/util/map_by_time.js | 77 ----------- src/util/map_by_timerange.js | 80 ----------- 11 files changed, 370 insertions(+), 429 deletions(-) create mode 100644 src/timeline/element.js create mode 100644 src/timeline/track.js rename src/util/{dom.js => browser.js} (100%) create mode 100644 src/util/list_by_time.js create mode 100644 src/util/list_by_timerange.js delete mode 100644 src/util/map_by_time.js delete mode 100644 src/util/map_by_timerange.js diff --git a/index.html b/index.html index aa5cef9..829a912 100644 --- a/index.html +++ b/index.html @@ -13,18 +13,20 @@
- - - - - - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js index d7586be..6fc169b 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -1,24 +1,8 @@ function Song() { - this.timePerWholeNote = 960; - this.length = 960 * 4; - - this.MIN_VALID_MIDI_PITCH = 3 * 12; - this.MAX_VALID_MIDI_PITCH = 8 * 12; - - this.notesById = []; - this.notesModified = []; - this.metersById = []; - - this.eventNoteAdded = new Callback(); - this.eventNoteModified = new Callback(); - this.eventNoteRemoved = new Callback(); - - this.eventMeterAdded = new Callback(); - this.eventMeterModified = new Callback(); - this.eventMeterRemoved = new Callback(); - - this.sanitize(); + this.length = 960 * 4; + this.notes = []; + this.meters = []; } @@ -28,137 +12,19 @@ Song.prototype.sanitize = function() } -Song.prototype.raiseAllAdded = function() +Song.prototype.setLength = function(length) { - for (var i = 0; i < this.notesById.length; i++) - { - if (this.notesById[i] != null) - this.eventNoteAdded.call(function (fn) { fn(i); }); - } - - for (var i = 0; i < this.metersById.length; i++) - { - if (this.metersById[i] != null) - this.eventMeterAdded.call(function (fn) { fn(i); }); - } -} - - -Song.prototype.raiseAllRemoved = function() -{ - for (var i = 0; i < this.notesById.length; i++) - { - if (this.notesById[i] != null) - this.eventNoteRemoved.call(function (fn) { fn(i); }); - } - - for (var i = 0; i < this.metersById.length; i++) - { - if (this.metersById[i] != null) - this.eventMeterRemoved.call(function (fn) { fn(i); }); - } -} - - -Song.prototype.applyModifications = function() -{ - for (var i = 0; i < this.notesModified.length; i++) - { - var modif = this.notesModified[i]; - this._noteSet(modif.id, modif.note, true); - } - - this.notesModified = []; -} - - -Song.prototype._clipNotes = function(note) -{ - // Check for overlapping notes and clip them. - for (var i = 0; i < this.notesById.length; i++) - { - var otherNote = this.notesById[i]; - if (otherNote == null) - continue; - - if (otherNote.pitch.midiPitch != note.pitch.midiPitch) - continue; - - if (!otherNote.timeRange.overlapsRange(note.timeRange)) - continue; - - this.noteRemove(i); - - var parts = otherNote.timeRange.getClippedParts(note.timeRange); - for (var p = 0; p < parts.length; p++) - { - var clippedNote = otherNote.clone(); - clippedNote.timeRange = parts[p]; - this.noteAdd(clippedNote); - } - } -} - - -Song.prototype._noteSet = function(id, note, asModification) -{ - this._clipNotes(note); - this.notesById[id] = note; - - if (asModification) - this.eventNoteModified.call(function (fn) { fn(id); }); - else - this.eventNoteAdded .call(function (fn) { fn(id); }); + this.length = length; } Song.prototype.noteAdd = function(note) { - var id = this.notesById.length; - this._noteSet(id, note, false); - - return id; -} - - -Song.prototype.noteRemove = function(id) -{ - this.eventNoteRemoved.call(function (fn) { fn(id); }); - this.notesById[id] = null; -} - - -Song.prototype.noteModify = function(id, note) -{ - this.notesById[id] = null; - this.notesModified.push({ id: id, note: note }); -} - - -Song.prototype.noteGet = function(id) -{ - return this.notesById[id]; + this.notes.push(note); } Song.prototype.meterAdd = function(meter) { - var id = this.metersById.length; - this.metersById[id] = meter; - - this.eventMeterAdded.call(function (fn) { fn(id); }); - return id; -} - - -Song.prototype.meterRemove = function(id) -{ - this.eventMeterRemoved.call(function (fn) { fn(id); }); - this.metersById[id] = null; -} - - -Song.prototype.meterGet = function(id) -{ - return this.metersById[id]; + this.meters.push(meter); } \ No newline at end of file diff --git a/src/timeline/element.js b/src/timeline/element.js new file mode 100644 index 0000000..d2b16e1 --- /dev/null +++ b/src/timeline/element.js @@ -0,0 +1,16 @@ +function Element() +{ + this.selected = false; + this.track = null; + this.timeRange = null; + + var that = this; + this.select = function() { that.track.elementSelect (this); }; + this.unselect = function() { that.track.elementUnselect(this); }; + this.modify = function() { that.track.elementModify (this); }; + + this.regions = []; + this.interactKind = 0; + this.interactTimeRange = null; + this.interactPitch = null; +} \ No newline at end of file diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index db963ae..d9f10f7 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -8,6 +8,11 @@ function Timeline(canvas) this.INTERACT_MOVE_PITCH = 0x2; this.INTERACT_STRETCH_TIME = 0x4; + this.TIME_PER_WHOLE_NOTE = 960; + this.MAX_VALID_LENGTH = 960 * 1024; + this.MIN_VALID_MIDI_PITCH = 3 * 12; + this.MAX_VALID_MIDI_PITCH = 8 * 12 - 1; + // Get canvas context. this.canvas = canvas; this.ctx = canvas.getContext("2d"); @@ -33,23 +38,17 @@ function Timeline(canvas) this.hoverRegion = null; this.selectedElements = []; - // Set up song and song events. - this.song = null; - - this.eventNoteAdded = new Callback(); - this.eventNoteModified = new Callback(); - this.eventNoteRemoved = new Callback(); - // Set up display metrics. this.scrollTime = 0; this.timeSnap = 960 / 16; this.timeToPixelsScaling = 100 / 960; - this.noteHeight = 10; + this.noteHeight = 12; this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; // Set up tracks. + this.length = 0; this.trackNotes = new TrackNotes(this); this.tracks = []; @@ -61,19 +60,12 @@ Timeline.prototype.setSong = function(song) { this.unselectAll(); - if (this.song != null) - this.song.raiseAllRemoved(); + this.length = song.length; - this.song = song; + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].setSong(song); - if (this.song != null) { - var that = this; - this.song.eventNoteAdded .add(function (id) { that.eventNoteAdded .call(function (fn) { fn(id); }); }); - this.song.eventNoteModified.add(function (id) { that.eventNoteModified.call(function (fn) { fn(id); }); }); - this.song.eventNoteRemoved .add(function (id) { that.eventNoteRemoved .call(function (fn) { fn(id); }); }); - this.song.raiseAllAdded(); - this.markDirtyAll(); - } + this.markDirtyAll(); } @@ -157,7 +149,7 @@ Timeline.prototype.handleMouseMove = function(ev) // ensuring that elements cannot fall out of bounds. this.mouseMoveDeltaTime = Math.max(-allEncompasingInteractTimeRange.start, - Math.min(this.song.length - allEncompasingInteractTimeRange.end, + Math.min(this.length - allEncompasingInteractTimeRange.end, mouseTimeDelta)); this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); @@ -191,8 +183,8 @@ Timeline.prototype.handleMouseMove = function(ev) // Calculate displacement, // ensuring that pitches cannot fall out of bounds. this.mouseMoveDeltaPitch = - Math.max(this.song.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, - Math.min(this.song.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, + Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, + Math.min(this.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, mousePitchDelta)); } } @@ -212,7 +204,7 @@ Timeline.prototype.handleMouseMove = function(ev) { var track = this.tracks[trackIndex]; - track.elements.enumerateOverlappingTime(mouseTime, function (index, elem) + track.elements.enumerateOverlappingTime(mouseTime, function (elem) { for (var e = 0; e < elem.regions.length; e++) { @@ -269,9 +261,11 @@ Timeline.prototype.handleMouseUp = function(ev) this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); for (var i = 0; i < this.selectedElements.length; i++) - this.selectedElements[i].modify(this.selectedElements[i]); + this.selectedElements[i].modify(); + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].applyModifications(); - this.song.applyModifications(); this.markDirtyAllSelectedElements(0); } } @@ -286,7 +280,7 @@ Timeline.prototype.handleMouseUp = function(ev) Timeline.prototype.markDirtyAll = function() { this.redrawDirtyTimeMin = -100; - this.redrawDirtyTimeMax = this.song.length + 100; + this.redrawDirtyTimeMax = this.length + 100; } @@ -321,7 +315,7 @@ Timeline.prototype.unselectAll = function() { for (var i = 0; i < this.selectedElements.length; i++) { - this.selectedElements[i].selected = false; + this.selectedElements[i].unselect(); this.markDirtyElement(this.selectedElements[i]); } @@ -335,7 +329,7 @@ Timeline.prototype.unselectAllOfDifferentKind = function(kind) { if ((this.selectedElements[i].interactKind & kind) == 0) { - this.selectedElements[i].selected = false; + this.selectedElements[i].unselect(); this.markDirtyElement(this.selectedElements[i]); this.selectedElements[i].splice(i, 1); } @@ -345,7 +339,7 @@ Timeline.prototype.unselectAllOfDifferentKind = function(kind) Timeline.prototype.select = function(elem) { - elem.selected = true; + elem.select(); this.selectedElements.push(elem); this.markDirtyElement(elem); } diff --git a/src/timeline/track.js b/src/timeline/track.js new file mode 100644 index 0000000..77649b3 --- /dev/null +++ b/src/timeline/track.js @@ -0,0 +1,16 @@ +function Track() +{ + +} + +Track.prototype.setSong = function(song) { } + +Track.prototype.handleScroll = function() { } + +Track.prototype.elementSelect = function(elem) { } +Track.prototype.elementUnselect = function(elem) { } +Track.prototype.elementModify = function(elem) { } +Track.prototype.applyModifications = function() { } + +Track.prototype.relayout = function() { } +Track.prototype.redraw = function(time1, time2) { } \ No newline at end of file diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index bc91527..790ff5a 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -2,80 +2,131 @@ function TrackNotes(timeline) { this.timeline = timeline; - var that = this; - this.timeline.eventNoteAdded .add(function (id) { that.onNoteAdded (id); }); - this.timeline.eventNoteModified.add(function (id) { that.onNoteModified(id); }); - this.timeline.eventNoteRemoved .add(function (id) { that.onNoteRemoved (id); }); - - this.y = 0; - this.height = 0; - + this.y = 0; + this.height = 0; this.scrollY = 0; - this.elements = new MapByTimeRange(); + this.elements = new ListByTimeRange(); + + this.selectedElements = []; + this.modifiedElements = []; } -TrackNotes.prototype.onNoteAdded = function(id) +TrackNotes.prototype = new Track(); + + +TrackNotes.prototype.setSong = function(song) { - var that = this; - - var elem = { - id: id, - selected: false, - timeRange: null, - - modify: function(elem) { that.elementModify(elem); }, - - regions: [], - interactKind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, - interactTimeRange: null, - interactPitch: null - }; + this.elements.clear(); + this.selectedElements = []; - this.elementRefresh(elem); - this.elements.add(id, elem); + for (var i = 0; i < song.notes.length; i++) + { + this._clipNotes(song.notes[i].timeRange, song.notes[i].pitch); + this._noteAdd(song.notes[i]); + } } -TrackNotes.prototype.onNoteModified = function(id) +TrackNotes.prototype._noteAdd = function(note) { - var elem = this.elements.get(id); + var elem = new Element(); + elem.track = this; + elem.note = note.clone(); + this.elementRefresh(elem); - this.elements.refresh(id); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); } -TrackNotes.prototype.onNoteRemoved = function(id) +TrackNotes.prototype._clipNotes = function(timeRange, pitch) { - this.timeline.markDirtyElement(this.elements.get(id)); - this.elements.remove(id); + // Check for overlapping notes and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingRange(timeRange, function (elem) + { + if (elem.note.pitch.midiPitch == pitch.midiPitch) + overlapping.push(elem); + }); + + for (var i = 0; i < overlapping.length; i++) + this.elements.remove(overlapping[i]); + + for (var i = 0; i < overlapping.length; i++) + { + var elem = overlapping[i]; + + var parts = elem.note.timeRange.getClippedParts(timeRange); + for (var p = 0; p < parts.length; p++) + { + var clippedNote = elem.note.clone(); + clippedNote.timeRange = parts[p]; + this._noteAdd(clippedNote); + } + } } -TrackNotes.prototype.handleScroll = function(elem) +TrackNotes.prototype.handleScroll = function() { this.scrollY = this.getModifiedScrollY(); this.timeline.markDirtyAll(); } -TrackNotes.prototype.elementModify = function(elem) +TrackNotes.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + this._clipNotes(elem.note.timeRange, elem.note.pitch); + } + + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); + } + + this.modifiedElements = []; +} + + +TrackNotes.prototype.elementSelect = function(elem) +{ + elem.selected = true; + this.selectedElements.push(elem); +} + + +TrackNotes.prototype.elementUnselect = function(elem) { - var note = this.timeline.song.noteGet(elem.id); + elem.selected = false; - var start = note.timeRange.start; - var duration = note.timeRange.duration(); - var pitch = note.pitch.midiPitch; + var index = this.selectedElements.indexOf(elem); - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) - start += this.timeline.mouseMoveDeltaTime; + if (index != -1) + this.selectedElements.splice(index, 1); +} + - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) - pitch += this.timeline.mouseMoveDeltaPitch; +TrackNotes.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.note = new Note( + new TimeRange(modifiedElem.start, modifiedElem.start + modifiedElem.duration), + new Pitch(modifiedElem.pitch)); - this.timeline.song.noteModify(elem.id, - new Note(new TimeRange(start, start + duration), new Pitch(pitch))); + this.modifiedElements.push(elem); } @@ -83,20 +134,19 @@ TrackNotes.prototype.elementRefresh = function(elem) { var toPixels = this.timeline.timeToPixelsScaling; var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; - var note = this.timeline.song.noteGet(elem.id); - - elem.timeRange = note.timeRange.clone(); - elem.interactTimeRange = note.timeRange.clone(); - elem.interactPitch = note.pitch.clone(); + elem.timeRange = elem.note.timeRange.clone(); + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH; + elem.interactTimeRange = elem.note.timeRange.clone(); + elem.interactPitch = elem.note.pitch.clone(); elem.regions = [ { kind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, - x: note.timeRange.start * toPixels, - y: this.height - noteHeight * (note.pitch.midiPitch - minPitch) - noteHeight, - width: note.timeRange.duration() * toPixels, + x: elem.note.timeRange.start * toPixels, + y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, + width: elem.note.timeRange.duration() * toPixels, height: noteHeight - 1 } ]; @@ -107,10 +157,8 @@ TrackNotes.prototype.relayout = function() { var that = this; - this.elements.enumerateAll(function (index, elem) - { - that.elementRefresh(elem); - }); + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); } @@ -120,12 +168,12 @@ TrackNotes.prototype.redraw = function(time1, time2) var ctx = this.timeline.ctx; var toPixels = this.timeline.timeToPixelsScaling; var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; - var maxPitch = this.timeline.song.MAX_VALID_MIDI_PITCH; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; var scrollY = this.getModifiedScrollY(); var xMin = time1 * toPixels; - var xMax = Math.min(time2, this.timeline.song.length) * toPixels; + var xMax = Math.min(time2, this.timeline.length) * toPixels; ctx.save(); @@ -150,26 +198,50 @@ TrackNotes.prototype.redraw = function(time1, time2) } // Draw notes. - this.elements.enumerateOverlappingRangeOrSelected(new TimeRange(time1, time2), function (index, elem) - { - that.drawNote(elem); - }); + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawNote(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawNote(this.selectedElements[i]); ctx.restore(); // Draw borders. ctx.strokeStyle = "#000000"; - ctx.strokeRect(0, 0, this.timeline.song.length * toPixels, this.height); + ctx.strokeRect(0, 0, this.timeline.length * toPixels, this.height); ctx.restore(); } +TrackNotes.prototype.getModifiedElement = function(elem) +{ + var start = elem.note.timeRange.start; + var duration = elem.note.timeRange.duration(); + var pitch = elem.note.pitch.midiPitch; + + if (elem.selected) + { + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + start += this.timeline.mouseMoveDeltaTime; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) + pitch += this.timeline.mouseMoveDeltaPitch; + } + + return { + start: start, + duration: duration, + pitch: pitch + }; +} + + TrackNotes.prototype.getModifiedScrollY = function() { var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; - var maxPitch = this.timeline.song.MAX_VALID_MIDI_PITCH; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; var scrollY = this.scrollY; @@ -189,25 +261,12 @@ TrackNotes.prototype.getModifiedScrollY = function() TrackNotes.prototype.drawNote = function(elem) { - var ctx = this.timeline.ctx; - var toPixels = this.timeline.timeToPixelsScaling; - var noteHeight = this.timeline.noteHeight; - var minPitch = this.timeline.song.MIN_VALID_MIDI_PITCH; - var note = this.timeline.song.noteGet(elem.id); - var scrollY = this.getModifiedScrollY(); - - var start = note.timeRange.start; - var duration = note.timeRange.duration(); - var pitch = note.pitch.midiPitch; - - if (elem.selected) - { - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) - start += this.timeline.mouseMoveDeltaTime; - - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) - pitch += this.timeline.mouseMoveDeltaPitch; - } + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var scrollY = this.getModifiedScrollY(); + var modifiedElem = this.getModifiedElement(elem); ctx.globalAlpha = 1; if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) @@ -215,9 +274,9 @@ TrackNotes.prototype.drawNote = function(elem) else ctx.fillStyle = "#ff0000"; - var x = 0.5 + Math.floor(start * toPixels); - var y = 0.5 + this.height - noteHeight * (pitch - minPitch); - var w = Math.floor(duration * toPixels - 1); + var x = 0.5 + Math.floor(modifiedElem.start * toPixels); + var y = 0.5 + this.height - noteHeight * (modifiedElem.pitch - minPitch); + var w = Math.floor(modifiedElem.duration * toPixels - 1); y = Math.max(-scrollY + 3, diff --git a/src/util/dom.js b/src/util/browser.js similarity index 100% rename from src/util/dom.js rename to src/util/browser.js diff --git a/src/util/list_by_time.js b/src/util/list_by_time.js new file mode 100644 index 0000000..f3944fa --- /dev/null +++ b/src/util/list_by_time.js @@ -0,0 +1,78 @@ +// TODO: Optimize with better data structures. +// +// Querying items contained in a given time range, or +// querying the first item at a time smaller than the given time, +// should be fast operations. +function ListByTime() +{ + this.items = []; +} + + +ListByTime.prototype.clear = function() +{ + this.items = []; +} + + +ListByTime.prototype.add = function(item) +{ + this.items.push(item); +} + + +ListByTime.prototype.remove = function(item) +{ + var index = this.items.indexOf(item); + + if (index != -1) + this.items.splice(index, 1); +} + + +ListByTime.prototype.enumerateAll = function(callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + callback(this.items[i]); + } +} + + +ListByTime.prototype.enumerateOverlappingRange = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (timeRange.overlapsTime(this.items[i].time)) + callback(this.items[i]); + } +} + + +ListByTime.prototype.getActiveAtTime = function(time) +{ + var item = null; + var minTimeSoFar = -1; + + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + var itemTime = this.items[i].time; + + if (minTimeSoFar == -1 || (itemTime < minTimeSoFar && itemTime >= time)) + { + item = this.items[i]; + minTimeSoFar = itemTime; + } + } + + return item; +} \ No newline at end of file diff --git a/src/util/list_by_timerange.js b/src/util/list_by_timerange.js new file mode 100644 index 0000000..72bfe16 --- /dev/null +++ b/src/util/list_by_timerange.js @@ -0,0 +1,67 @@ +// TODO: Optimize with better data structures. +// +// Querying items contained in a given time range +// should be fast. +function ListByTimeRange() +{ + this.items = []; +} + + +ListByTimeRange.prototype.clear = function() +{ + this.items = []; +} + + +ListByTimeRange.prototype.add = function(item) +{ + this.items.push(item); +} + + +ListByTimeRange.prototype.remove = function(item) +{ + var index = this.items.indexOf(item); + + if (index != -1) + this.items.splice(index, 1); +} + + +ListByTimeRange.prototype.enumerateAll = function(callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + callback(this.items[i]); + } +} + + +ListByTimeRange.prototype.enumerateOverlappingTime = function(time, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (this.items[i].timeRange.overlapsTime(time)) + callback(this.items[i]); + } +} + + +ListByTimeRange.prototype.enumerateOverlappingRange = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (timeRange.overlapsRange(this.items[i].timeRange)) + callback(this.items[i]); + } +} \ No newline at end of file diff --git a/src/util/map_by_time.js b/src/util/map_by_time.js deleted file mode 100644 index fbba126..0000000 --- a/src/util/map_by_time.js +++ /dev/null @@ -1,77 +0,0 @@ -// TODO: Optimize with better data structures. -function MapByTime() -{ - this.items = []; -} - - -MapByTime.prototype.add = function(id, item) -{ - this.items[id] = item; -} - - -MapByTime.prototype.remove = function(id) -{ - this.items[id] = null; -} - - -MapByTime.prototype.get = function(id) -{ - return this.items[id]; -} - - -MapByTime.prototype.refresh = function(id) -{ - // Do nothing for this unoptimized implementation. -} - - -MapByTime.prototype.enumerateAll = function(callback) -{ - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - callback(i, this.items[i]); - } -} - - -MapByTime.prototype.enumerateOverlappingRange = function(timeRange, callback) -{ - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - if (timeRange.overlapsTime(this.items[i].time)) - callback(i, this.items[i]); - } -} - - -MapByTime.prototype.getActiveAtTime = function(time) -{ - var index = -1; - var minTimeSoFar = -1; - - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - var itemTime = this.items[i].time; - - if (minTimeSoFar == -1 || (itemTime < minTimeSoFar && itemTime >= time)) - { - index = i; - minTimeSoFar = itemTime; - } - } - - return index; -} \ No newline at end of file diff --git a/src/util/map_by_timerange.js b/src/util/map_by_timerange.js deleted file mode 100644 index c30064b..0000000 --- a/src/util/map_by_timerange.js +++ /dev/null @@ -1,80 +0,0 @@ -// TODO: Optimize with better data structures. -function MapByTimeRange() -{ - this.items = []; -} - - -MapByTimeRange.prototype.add = function(id, item) -{ - this.items[id] = item; -} - - -MapByTimeRange.prototype.remove = function(id) -{ - this.items[id] = null; -} - - -MapByTimeRange.prototype.get = function(id) -{ - return this.items[id]; -} - - -MapByTimeRange.prototype.refresh = function(id) -{ - // Do nothing for this unoptimized implementation. -} - - -MapByTimeRange.prototype.enumerateAll = function(callback) -{ - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - callback(i, this.items[i]); - } -} - - -MapByTimeRange.prototype.enumerateOverlappingTime = function(time, callback) -{ - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - if (this.items[i].timeRange.overlapsTime(time)) - callback(i, this.items[i]); - } -} - - -MapByTimeRange.prototype.enumerateOverlappingRange = function(timeRange, callback) -{ - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - if (timeRange.overlapsRange(this.items[i].timeRange)) - callback(i, this.items[i]); - } -} - - -MapByTimeRange.prototype.enumerateOverlappingRangeOrSelected = function(timeRange, callback) -{ - for (var i = 0; i < this.items.length; i++) - { - if (this.items[i] == null) - continue; - - if (this.items[i].selected || timeRange.overlapsRange(this.items[i].timeRange)) - callback(i, this.items[i]); - } -} \ No newline at end of file From 122526bc67208de1a1185c98357e8493157169ef Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 21 Aug 2016 16:38:24 -0300 Subject: [PATCH 32/46] add length track; add horizontal scrolling --- index.html | 8 +- src/timeline/element.js | 25 +++- src/timeline/timeline.js | 270 +++++++++++++++++++++-------------- src/timeline/track_length.js | 163 +++++++++++++++++++++ src/timeline/track_notes.js | 50 +++---- src/util/browser.js | 9 -- 6 files changed, 376 insertions(+), 149 deletions(-) create mode 100644 src/timeline/track_length.js delete mode 100644 src/util/browser.js diff --git a/index.html b/index.html index 829a912..0d34ac1 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Music Analysis - +
@@ -20,13 +20,13 @@ + - - - \ No newline at end of file + + diff --git a/src/timeline/element.js b/src/timeline/element.js index d2b16e1..b783cac 100644 --- a/src/timeline/element.js +++ b/src/timeline/element.js @@ -4,13 +4,28 @@ function Element() this.track = null; this.timeRange = null; - var that = this; - this.select = function() { that.track.elementSelect (this); }; - this.unselect = function() { that.track.elementUnselect(this); }; - this.modify = function() { that.track.elementModify (this); }; - this.regions = []; this.interactKind = 0; this.interactTimeRange = null; this.interactPitch = null; + + var that = this; + + this.select = function() + { + this.selected = true; + this.track.selectedElements.push(this); + }; + + this.unselect = function() + { + this.selected = false; + + var index = this.track.selectedElements.indexOf(this); + + if (index != -1) + this.track.selectedElements.splice(index, 1); + }; + + this.modify = function() { that.track.elementModify(this); }; } \ No newline at end of file diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index d9f10f7..e4ca3b0 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -1,57 +1,64 @@ function Timeline(canvas) { var that = this; - + // Set up constants. this.INTERACT_NONE = 0x0; this.INTERACT_MOVE_TIME = 0x1; this.INTERACT_MOVE_PITCH = 0x2; this.INTERACT_STRETCH_TIME = 0x4; - + this.TIME_PER_WHOLE_NOTE = 960; this.MAX_VALID_LENGTH = 960 * 1024; this.MIN_VALID_MIDI_PITCH = 3 * 12; this.MAX_VALID_MIDI_PITCH = 8 * 12 - 1; - + this.REDRAW_TIME_MARGIN = 50; + // Get canvas context. this.canvas = canvas; this.ctx = canvas.getContext("2d"); - + this.canvasWidth = 0; this.canvasHeight = 0; - + // Set up mouse/keyboard interaction. this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; window.onmousemove = function(ev) { that.handleMouseMove(ev); }; window.onmouseup = function(ev) { that.handleMouseUp(ev); }; //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; - - this.mouseDown = false; - this.mouseDownPos = null; - this.mouseDownTrack = null; - this.mouseAction = this.INTERACT_NONE; - this.mouseMoveDeltaTime = 0; - this.mouseMovePitch = 0; - this.mouseMoveScrollY = 0; - + + this.mouseDown = false; + this.mouseDownPos = null; + this.mouseDownTrack = null; + this.mouseDownScrollTime = 0; + this.mouseAction = this.INTERACT_NONE; + this.mouseMoveDeltaTime = 0; + this.mouseMovePitch = 0; + this.mouseMoveScrollY = 0; + this.hoverElement = null; this.hoverRegion = null; this.selectedElements = []; - + // Set up display metrics. this.scrollTime = 0; + this.firstTimeVisible = 0; + this.lastTimeVisible = 0; this.timeSnap = 960 / 16; this.timeToPixelsScaling = 100 / 960; this.noteHeight = 12; - + this.lastTrackBottomY = 0; + this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; - + // Set up tracks. this.length = 0; - this.trackNotes = new TrackNotes(this); - + this.trackLength = new TrackLength(this); + this.trackNotes = new TrackNotes(this); + this.tracks = []; + this.tracks.push(this.trackLength); this.tracks.push(this.trackNotes); } @@ -59,48 +66,61 @@ function Timeline(canvas) Timeline.prototype.setSong = function(song) { this.unselectAll(); - + this.length = song.length; - + for (var i = 0; i < this.tracks.length; i++) this.tracks[i].setSong(song); - + this.markDirtyAll(); } +Timeline.prototype.mouseToClient = function(ev) +{ + var rect = this.canvas.getBoundingClientRect(); + + return { + x: ev.clientX - rect.left, + xScrolled: ev.clientX - rect.left + this.scrollTime * this.timeToPixelsScaling, + y: ev.clientY - rect.top + }; +} + + Timeline.prototype.handleMouseDown = function(ev) { var that = this; - + ev.preventDefault(); - + var ctrl = ev.ctrlKey; - var mousePos = mouseToClient(this.canvas, ev); - + var mousePos = this.mouseToClient(ev); + // Handle multi-selection with Ctrl. if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) this.unselectAll(); - + if (this.hoverElement != null) { // Handle selection of element under mouse. if (!this.hoverElement.selected) this.select(this.hoverElement); - + this.unselectAllOfDifferentKind(this.hoverElement.interactKind); - + this.mouseAction = this.hoverElement.interactKind; this.markDirtyAllSelectedElements(0); } else this.mouseAction = this.INTERACT_NONE; - + var trackIndex = this.getTrackIndexAtY(mousePos.y); - this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); - + this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); + this.mouseDown = true; this.mouseDownPos = mousePos; + this.mouseDownScrollTime = this.scrollTime; this.mouseMoveDeltaTime = 0; this.mouseMoveDeltaPitch = 0; this.mouseMoveScrollY = 0; @@ -111,49 +131,70 @@ Timeline.prototype.handleMouseDown = function(ev) Timeline.prototype.handleMouseMove = function(ev) { var that = this; - + ev.preventDefault(); - - var mousePos = mouseToClient(this.canvas, ev); - var mouseTime = snap(mousePos.x / this.timeToPixelsScaling, this.timeSnap); - + + var mousePos = this.mouseToClient(ev); + var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); + // Handle dragging with the mouse. if (this.mouseDown) { - var mouseTimeDelta = (mousePos.x - this.mouseDownPos.x) / this.timeToPixelsScaling; + var mouseTimeDelta = (mousePos.xScrolled - this.mouseDownPos.xScrolled) / this.timeToPixelsScaling; var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); - + // Handle scrolling. if (this.mouseAction == this.INTERACT_NONE) { + var scrollTimeDelta = (this.mouseDownPos.x - mousePos.x) / this.timeToPixelsScaling; + this.scrollTime = + Math.max(0, + Math.min(this.length - 960, + this.mouseDownScrollTime + scrollTimeDelta)); + + this.firstTimeVisible = this.scrollTime; + this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; + this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); this.markDirtyAll(); } - + if (this.selectedElements.length != 0) { // Handle time displacement. if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) { // Get merged time ranges of all selected elements. - var allEncompasingInteractTimeRange = this.selectedElements[0].interactTimeRange.clone(); - - for (var i = 1; i < this.selectedElements.length; i++) - allEncompasingInteractTimeRange.merge(this.selectedElements[i].interactTimeRange); - + var allEncompasingInteractTimeRange = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + var timeRange = this.selectedElements[i].interactTimeRange; + if (timeRange == null) + continue; + + if (allEncompasingInteractTimeRange == null) + allEncompasingInteractTimeRange = timeRange.clone(); + else + allEncompasingInteractTimeRange.merge(timeRange); + } + // Mark elements' previous positions as dirty, // to redraw over when they move away. this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - + // Calculate displacement, // ensuring that elements cannot fall out of bounds. - this.mouseMoveDeltaTime = - Math.max(-allEncompasingInteractTimeRange.start, - Math.min(this.length - allEncompasingInteractTimeRange.end, - mouseTimeDelta)); - + if (allEncompasingInteractTimeRange == null) + this.mouseMoveDeltaTime = mouseTimeDelta; + else + this.mouseMoveDeltaTime = + Math.max(-allEncompasingInteractTimeRange.start, + Math.min(this.length - allEncompasingInteractTimeRange.end, + mouseTimeDelta)); + this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); - + // Mark elements' new positions as dirty, // to redraw them at wherever they move to. this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); @@ -161,59 +202,59 @@ Timeline.prototype.handleMouseMove = function(ev) // If not displacing time, mark elements as dirty in their current positions. else this.markDirtyAllSelectedElements(0); - + // Handle pitch displacement. if ((this.mouseAction & this.INTERACT_MOVE_PITCH) != 0) { // Get the pitch range of all selected elements. var pitchMin = this.selectedElements[0].interactPitch.clone(); var pitchMax = this.selectedElements[0].interactPitch.clone(); - + for (var i = 1; i < this.selectedElements.length; i++) { var pitch = this.selectedElements[i].interactPitch.midiPitch; - + if (pitch < pitchMin.midiPitch) pitchMin = new Pitch(pitch); - + if (pitch > pitchMax.midiPitch) pitchMax = new Pitch(pitch); } - + // Calculate displacement, // ensuring that pitches cannot fall out of bounds. - this.mouseMoveDeltaPitch = + this.mouseMoveDeltaPitch = Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, Math.min(this.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, mousePitchDelta)); } } } - + // If mouse is not down, just handle hovering. else { if (this.hoverElement != null) this.markDirtyElement(this.hoverElement); - + this.hoverElement = null; this.hoverRegion = null; - + var trackIndex = this.getTrackIndexAtY(mousePos.y); if (trackIndex != -1) { var track = this.tracks[trackIndex]; - + track.elements.enumerateOverlappingTime(mouseTime, function (elem) { for (var e = 0; e < elem.regions.length; e++) { var region = elem.regions[e]; - - if (mousePos.x >= region.x && - mousePos.x <= region.x + region.width && - mousePos.y >= region.y + track.y + track.scrollY && - mousePos.y <= region.y + region.height + track.y + track.scrollY) + + if (mousePos.xScrolled >= region.x && + mousePos.xScrolled <= region.x + region.width && + mousePos.y >= region.y + track.y + track.scrollY && + mousePos.y <= region.y + region.height + track.y + track.scrollY) { that.hoverElement = elem; that.hoverRegion = region; @@ -221,14 +262,14 @@ Timeline.prototype.handleMouseMove = function(ev) } }); } - + if (this.hoverElement != null) this.markDirtyElement(this.hoverElement); - + var regionKind = this.INTERACT_NONE; if (this.hoverRegion != null) regionKind = this.hoverRegion.kind; - + if ((regionKind & this.INTERACT_MOVE_TIME != 0) || (regionKind & this.INTERACT_MOVE_PITCH != 0)) this.canvas.style.cursor = "pointer"; @@ -237,7 +278,7 @@ Timeline.prototype.handleMouseMove = function(ev) else this.canvas.style.cursor = "default"; } - + this.redraw(); } @@ -245,9 +286,7 @@ Timeline.prototype.handleMouseMove = function(ev) Timeline.prototype.handleMouseUp = function(ev) { ev.preventDefault(); - - var mousePos = mouseToClient(this.canvas, ev); - + // Handle releasing the mouse after dragging. if (this.mouseDown) { @@ -259,38 +298,44 @@ Timeline.prototype.handleMouseUp = function(ev) else { this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - + for (var i = 0; i < this.selectedElements.length; i++) this.selectedElements[i].modify(); - + for (var i = 0; i < this.tracks.length; i++) this.tracks[i].applyModifications(); - + this.markDirtyAllSelectedElements(0); } } - - this.mouseDown = false; - this.mouseAction = this.INTERACT_NONE; - this.mouseDownTrack = null; + + this.mouseDown = false; + this.mouseAction = this.INTERACT_NONE; + this.mouseDownTrack = null; this.redraw(); } Timeline.prototype.markDirtyAll = function() { - this.redrawDirtyTimeMin = -100; - this.redrawDirtyTimeMax = this.length + 100; + this.redrawDirtyTimeMin = this.firstTimeVisible - 100; + this.redrawDirtyTimeMax = this.lastTimeVisible + 100; } Timeline.prototype.markDirty = function(timeStart, timeEnd) { - if (this.redrawDirtyTimeMin == -1 || timeStart - 10 < this.redrawDirtyTimeMin) - this.redrawDirtyTimeMin = timeStart - 10; - - if (this.redrawDirtyTimeMax == -1 || timeEnd + 10 > this.redrawDirtyTimeMax) - this.redrawDirtyTimeMax = timeEnd + 10; + if (this.redrawDirtyTimeMin == -1 || + timeStart - this.REDRAW_TIME_MARGIN < this.redrawDirtyTimeMin) + { + this.redrawDirtyTimeMin = timeStart - this.REDRAW_TIME_MARGIN; + } + + if (this.redrawDirtyTimeMax == -1 || + timeEnd + this.REDRAW_TIME_MARGIN > this.redrawDirtyTimeMax) + { + this.redrawDirtyTimeMax = timeEnd + this.REDRAW_TIME_MARGIN; + } } @@ -318,7 +363,7 @@ Timeline.prototype.unselectAll = function() this.selectedElements[i].unselect(); this.markDirtyElement(this.selectedElements[i]); } - + this.selectedElements = []; } @@ -350,11 +395,11 @@ Timeline.prototype.getTrackIndexAtY = function(y) for (var i = 0; i < this.tracks.length; i++) { var track = this.tracks[i]; - + if (y >= track.y && y < track.y + track.height) return i; } - + return -1; } @@ -363,12 +408,22 @@ Timeline.prototype.relayout = function() { this.canvasWidth = parseFloat(this.canvas.width); this.canvasHeight = parseFloat(this.canvas.height); + + this.trackLength.y = 5; + this.trackLength.height = 20; + + this.trackNotes.y = 30; + this.trackNotes.height = this.canvasHeight - 35; - this.trackNotes.y = 5; - this.trackNotes.height = this.canvasHeight - 10; - + this.lastTrackBottomY = this.trackNotes.y + this.trackNotes.height; + for (var i = 0; i < this.tracks.length; i++) this.tracks[i].relayout(); + + this.firstTimeVisible = this.scrollTime; + this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; + + this.markDirtyAll(); } @@ -377,38 +432,39 @@ Timeline.prototype.redraw = function() // Return early if nothing dirty. if (this.redrawDirtyTimeMin == -1 || this.redrawDirtyTimeMax == -1) return; - + this.ctx.save(); - + this.ctx.translate(Math.floor(-this.scrollTime * this.timeToPixelsScaling), 0); + // Restrict drawing to dirty region. this.ctx.beginPath(); this.ctx.rect( - this.redrawDirtyTimeMin * this.timeToPixelsScaling + 1, + Math.floor((this.redrawDirtyTimeMin + this.REDRAW_TIME_MARGIN) * this.timeToPixelsScaling) + 0.5, 0, - (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin) * this.timeToPixelsScaling, + (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin - this.REDRAW_TIME_MARGIN * 2) * this.timeToPixelsScaling, this.canvasHeight); this.ctx.clip(); - + // Clear background. this.ctx.fillStyle = "#ffffff"; this.ctx.fillRect( - this.redrawDirtyTimeMin * this.timeToPixelsScaling, + Math.floor(this.redrawDirtyTimeMin * this.timeToPixelsScaling) + 0.5, 0, (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin) * this.timeToPixelsScaling, this.canvasHeight); - - // Draw tracks. - for (var i = 0; i < this.tracks.length; i++) + + // Draw tracks in reverse order. + for (var i = this.tracks.length - 1; i >= 0; i--) { this.ctx.save(); this.ctx.translate(0, this.tracks[i].y); - + this.tracks[i].redraw(this.redrawDirtyTimeMin, this.redrawDirtyTimeMax); - + this.ctx.restore(); } - + this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; this.ctx.restore(); -} \ No newline at end of file +} diff --git a/src/timeline/track_length.js b/src/timeline/track_length.js new file mode 100644 index 0000000..7398686 --- /dev/null +++ b/src/timeline/track_length.js @@ -0,0 +1,163 @@ +function TrackLength(timeline) +{ + this.timeline = timeline; + + this.y = 0; + this.height = 0; + this.scrollY = 0; + + this.elements = new ListByTimeRange(); + this.selectedElements = []; + + this.LENGTH_KNOB_WIDTH = 10; + this.lengthKnob = null; +} + + +TrackLength.prototype = new Track(); + + +TrackLength.prototype.setSong = function(song) +{ + this.elements.clear(); + + this.lengthKnob = new Element(); + this.lengthKnob.track = this; + this.lengthKnob.length = 0; + + this.elementRefresh(this.lengthKnob); + this.elements.add(this.lengthKnob); +} + + +TrackLength.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + + var modifiedElem = this.getModifiedLengthKnob(elem); + + elem.length = modifiedElem.length; + this.timeline.length = modifiedElem.length; + + this.elementRefresh(elem); + this.elements.add(elem); + + this.timeline.markDirtyAll(); +} + + +TrackLength.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.length = this.timeline.length; + elem.interactKind = this.timeline.INTERACT_MOVE_TIME; + elem.timeRange = new TimeRange( + elem.length - this.LENGTH_KNOB_WIDTH / 2 / toPixels, + elem.length + this.LENGTH_KNOB_WIDTH / 2 / toPixels); + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.length * toPixels - this.LENGTH_KNOB_WIDTH / 2, + y: 0, + width: this.LENGTH_KNOB_WIDTH, + height: this.height + } + ]; +} + + +TrackLength.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackLength.prototype.redraw = function(time1, time2) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + ctx.save(); + + ctx.translate(0.5, 0.5); + + // Draw markers. + ctx.strokeStyle = "#444444"; + ctx.beginPath(); + ctx.moveTo(0, this.height / 2); + ctx.lineTo(this.timeline.length * toPixels, this.height / 2); + ctx.stroke(); + + ctx.strokeStyle = "#aaaaaa"; + ctx.beginPath(); + ctx.moveTo(this.timeline.length * toPixels, this.height / 2); + ctx.lineTo(this.timeline.lastTimeVisible * toPixels, this.height / 2); + ctx.stroke(); + + ctx.fillStyle = "#ffffff"; + ctx.globalAlpha = 0.5; + ctx.fillRect( + Math.min(this.timeline.length, this.lengthKnob.length) * toPixels, + this.height, + Math.abs(this.timeline.length - this.lengthKnob.length) * toPixels, + this.timeline.lastTrackBottomY - (this.y + this.height)); + ctx.globalAlpha = 1; + + // Draw length knob. + this.drawLengthKnob(this.lengthKnob); + + ctx.restore(); +} + + +TrackLength.prototype.getModifiedLengthKnob = function(elem) +{ + var length = elem.length; + + if (elem.selected) + { + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + length += this.timeline.mouseMoveDeltaTime; + } + + return { + length: length + }; +} + + + +TrackLength.prototype.drawLengthKnob = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var modifiedKnob = this.getModifiedLengthKnob(elem); + + var col = "#000000"; + if (elem.selected) + col = "#888888"; + else if (this.timeline.hoverElement == elem) + col = "#aaaaaa"; + + ctx.fillStyle = col; + ctx.strokeStyle = col; + + var x = Math.floor(modifiedKnob.length * toPixels); + + ctx.fillRect( + Math.floor(x - this.LENGTH_KNOB_WIDTH / 2) - 0.5, + 0.5, + this.LENGTH_KNOB_WIDTH, + this.height); + + ctx.beginPath(); + ctx.moveTo(x, this.height / 2); + ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); + ctx.stroke(); +} diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 790ff5a..4bd8ad2 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -98,24 +98,6 @@ TrackNotes.prototype.applyModifications = function() } -TrackNotes.prototype.elementSelect = function(elem) -{ - elem.selected = true; - this.selectedElements.push(elem); -} - - -TrackNotes.prototype.elementUnselect = function(elem) -{ - elem.selected = false; - - var index = this.selectedElements.indexOf(elem); - - if (index != -1) - this.selectedElements.splice(index, 1); -} - - TrackNotes.prototype.elementModify = function(elem) { this.elements.remove(elem); @@ -172,13 +154,14 @@ TrackNotes.prototype.redraw = function(time1, time2) var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; var scrollY = this.getModifiedScrollY(); - var xMin = time1 * toPixels; - var xMax = Math.min(time2, this.timeline.length) * toPixels; + var xMin = Math.floor(time1 * toPixels); + var xLen = Math.floor(this.timeline.length * toPixels); + var xMax = Math.floor(time2 * toPixels); ctx.save(); ctx.beginPath(); - ctx.rect(xMin, 0, xMax - xMin + 1, this.height + 1); + ctx.rect(xMin - 10, 0, xMax - xMin + 20, this.height + 1); ctx.clip(); ctx.translate(0.5, 0.5); @@ -187,13 +170,20 @@ TrackNotes.prototype.redraw = function(time1, time2) ctx.translate(0, scrollY); // Draw pitch rows. - ctx.fillStyle = "#e4e4e4"; for (var i = minPitch; i <= maxPitch; i++) { + ctx.fillStyle = "#cccccc"; + ctx.fillRect( + xLen, + this.height - noteHeight * (i - minPitch), + xMax - xLen, + noteHeight - 0.5); + + ctx.fillStyle = "#e4e4e4"; ctx.fillRect( xMin, this.height - noteHeight * (i - minPitch), - xMax - xMin, + xLen - xMin, noteHeight - 0.5); } @@ -208,7 +198,19 @@ TrackNotes.prototype.redraw = function(time1, time2) // Draw borders. ctx.strokeStyle = "#000000"; - ctx.strokeRect(0, 0, this.timeline.length * toPixels, this.height); + ctx.beginPath(); + ctx.moveTo(xMin, 0); + ctx.lineTo(xMax, 0); + + ctx.moveTo(xMin, this.height); + ctx.lineTo(xMax, this.height); + + ctx.moveTo(0, 0); + ctx.lineTo(0, this.height); + + ctx.moveTo(xLen, 0); + ctx.lineTo(xLen, this.height); + ctx.stroke(); ctx.restore(); } diff --git a/src/util/browser.js b/src/util/browser.js deleted file mode 100644 index 179e6be..0000000 --- a/src/util/browser.js +++ /dev/null @@ -1,9 +0,0 @@ -function mouseToClient(elem, ev) -{ - var rect = elem.getBoundingClientRect(); - - return { - x: ev.clientX - rect.left, - y: ev.clientY - rect.top - }; -} \ No newline at end of file From cafa747de4dade98f53f1f0b4a859e37cde08bf3 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 21 Aug 2016 22:46:37 -0300 Subject: [PATCH 33/46] add meter change track --- index.html | 1 + main.js | 2 + src/song/meter.js | 9 ++ src/song/song.js | 6 - src/timeline/timeline.js | 22 ++-- src/timeline/track.js | 9 +- src/timeline/track_length.js | 71 +++++++---- src/timeline/track_meters.js | 231 +++++++++++++++++++++++++++++++++++ src/timeline/track_notes.js | 30 +++-- src/util/list_by_time.js | 49 +++++++- 10 files changed, 380 insertions(+), 50 deletions(-) create mode 100644 src/timeline/track_meters.js diff --git a/index.html b/index.html index 0d34ac1..702851d 100644 --- a/index.html +++ b/index.html @@ -21,6 +21,7 @@ + diff --git a/main.js b/main.js index 7e73105..7012d74 100644 --- a/main.js +++ b/main.js @@ -8,6 +8,8 @@ function main() canvas.width = div.clientWidth; var song = new Song(); + song.meterAdd(new Meter(0, 4, 4)); + song.meterAdd(new Meter(720, 3, 4)); song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(3 * 12 + 0))); song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(3 * 12 + 1))); song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(3 * 12 + 2))); diff --git a/src/song/meter.js b/src/song/meter.js index 3febddd..d19c2ca 100644 --- a/src/song/meter.js +++ b/src/song/meter.js @@ -3,4 +3,13 @@ function Meter(time, numerator, denominator) this.time = time; this.numerator = numerator; this.denominator = denominator; +} + + +Meter.prototype.clone = function() +{ + return new Meter( + this.time, + this.numerator, + this.denominator); } \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js index 6fc169b..dc4f2f9 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -6,12 +6,6 @@ function Song() } -Song.prototype.sanitize = function() -{ - this.meterAdd(new Meter(0, 4, 4)); -} - - Song.prototype.setLength = function(length) { this.length = length; diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index e4ca3b0..850d88a 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -12,6 +12,7 @@ function Timeline(canvas) this.MAX_VALID_LENGTH = 960 * 1024; this.MIN_VALID_MIDI_PITCH = 3 * 12; this.MAX_VALID_MIDI_PITCH = 8 * 12 - 1; + this.OFFSET_X = 10; this.REDRAW_TIME_MARGIN = 50; // Get canvas context. @@ -55,10 +56,12 @@ function Timeline(canvas) // Set up tracks. this.length = 0; this.trackLength = new TrackLength(this); + this.trackMeters = new TrackMeters(this); this.trackNotes = new TrackNotes(this); this.tracks = []; this.tracks.push(this.trackLength); + this.tracks.push(this.trackMeters); this.tracks.push(this.trackNotes); } @@ -81,8 +84,8 @@ Timeline.prototype.mouseToClient = function(ev) var rect = this.canvas.getBoundingClientRect(); return { - x: ev.clientX - rect.left, - xScrolled: ev.clientX - rect.left + this.scrollTime * this.timeToPixelsScaling, + x: ev.clientX - rect.left - this.OFFSET_X, + xScrolled: ev.clientX - rect.left - this.OFFSET_X + this.scrollTime * this.timeToPixelsScaling, y: ev.clientY - rect.top }; } @@ -152,7 +155,7 @@ Timeline.prototype.handleMouseMove = function(ev) Math.min(this.length - 960, this.mouseDownScrollTime + scrollTimeDelta)); - this.firstTimeVisible = this.scrollTime; + this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); @@ -412,15 +415,18 @@ Timeline.prototype.relayout = function() this.trackLength.y = 5; this.trackLength.height = 20; - this.trackNotes.y = 30; - this.trackNotes.height = this.canvasHeight - 35; + this.trackMeters.y = 25; + this.trackMeters.height = 20; + + this.trackNotes.y = 50; + this.trackNotes.height = this.canvasHeight - 55; this.lastTrackBottomY = this.trackNotes.y + this.trackNotes.height; for (var i = 0; i < this.tracks.length; i++) this.tracks[i].relayout(); - this.firstTimeVisible = this.scrollTime; + this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; this.markDirtyAll(); @@ -434,7 +440,9 @@ Timeline.prototype.redraw = function() return; this.ctx.save(); - this.ctx.translate(Math.floor(-this.scrollTime * this.timeToPixelsScaling), 0); + this.ctx.translate( + Math.floor(this.OFFSET_X - this.scrollTime * this.timeToPixelsScaling), + 0); // Restrict drawing to dirty region. this.ctx.beginPath(); diff --git a/src/timeline/track.js b/src/timeline/track.js index 77649b3..34cb503 100644 --- a/src/timeline/track.js +++ b/src/timeline/track.js @@ -1,6 +1,13 @@ function Track() { - + this.y = 0; + this.height = 0; + this.scrollY = 0; + + this.elements = null; + + this.selectedElements = []; + this.modifiedElements = []; } Track.prototype.setSong = function(song) { } diff --git a/src/timeline/track_length.js b/src/timeline/track_length.js index 7398686..1fee24c 100644 --- a/src/timeline/track_length.js +++ b/src/timeline/track_length.js @@ -1,13 +1,7 @@ function TrackLength(timeline) { this.timeline = timeline; - - this.y = 0; - this.height = 0; - this.scrollY = 0; - - this.elements = new ListByTimeRange(); - this.selectedElements = []; + this.elements = new ListByTimeRange(); this.LENGTH_KNOB_WIDTH = 10; this.lengthKnob = null; @@ -39,6 +33,8 @@ TrackLength.prototype.elementModify = function(elem) elem.length = modifiedElem.length; this.timeline.length = modifiedElem.length; + // FIXME: Clip elements of all tracks to the new length. + this.elementRefresh(elem); this.elements.add(elem); @@ -50,11 +46,12 @@ TrackLength.prototype.elementRefresh = function(elem) { var toPixels = this.timeline.timeToPixelsScaling; - elem.length = this.timeline.length; - elem.interactKind = this.timeline.INTERACT_MOVE_TIME; - elem.timeRange = new TimeRange( + elem.timeRange = new TimeRange( elem.length - this.LENGTH_KNOB_WIDTH / 2 / toPixels, elem.length + this.LENGTH_KNOB_WIDTH / 2 / toPixels); + + elem.length = this.timeline.length; + elem.interactKind = this.timeline.INTERACT_MOVE_TIME; elem.regions = [ { @@ -79,14 +76,15 @@ TrackLength.prototype.relayout = function() TrackLength.prototype.redraw = function(time1, time2) { - var ctx = this.timeline.ctx; - var toPixels = this.timeline.timeToPixelsScaling; + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; ctx.save(); ctx.translate(0.5, 0.5); - // Draw markers. + // Draw track line. ctx.strokeStyle = "#444444"; ctx.beginPath(); ctx.moveTo(0, this.height / 2); @@ -98,16 +96,24 @@ TrackLength.prototype.redraw = function(time1, time2) ctx.moveTo(this.timeline.length * toPixels, this.height / 2); ctx.lineTo(this.timeline.lastTimeVisible * toPixels, this.height / 2); ctx.stroke(); - - ctx.fillStyle = "#ffffff"; - ctx.globalAlpha = 0.5; - ctx.fillRect( - Math.min(this.timeline.length, this.lengthKnob.length) * toPixels, - this.height, - Math.abs(this.timeline.length - this.lengthKnob.length) * toPixels, - this.timeline.lastTrackBottomY - (this.y + this.height)); - ctx.globalAlpha = 1; + // Draw beat markers. + this.timeline.trackMeters.enumerateBeatsAtRange(new TimeRange(time1, time2), function (time, isStrong) + { + var x = Math.floor(time * toPixels); + var h = (isStrong ? 4 : 2); + + if (x > that.timeline.length) + ctx.strokeStyle = "#aaaaaa"; + else + ctx.strokeStyle = "#444444"; + + ctx.beginPath(); + ctx.moveTo(x, that.height / 2 - h); + ctx.lineTo(x, that.height / 2 + h); + ctx.stroke(); + }); + // Draw length knob. this.drawLengthKnob(this.lengthKnob); @@ -124,6 +130,11 @@ TrackLength.prototype.getModifiedLengthKnob = function(elem) if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) length += this.timeline.mouseMoveDeltaTime; } + + length = + Math.max(0, + Math.min(this.timeline.MAX_VALID_LENGTH, + length)); return { length: length @@ -154,10 +165,24 @@ TrackLength.prototype.drawLengthKnob = function(elem) Math.floor(x - this.LENGTH_KNOB_WIDTH / 2) - 0.5, 0.5, this.LENGTH_KNOB_WIDTH, - this.height); + this.height - 1); ctx.beginPath(); ctx.moveTo(x, this.height / 2); ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); ctx.stroke(); + + // Draw create/delete overlay. + if (modifiedKnob.length < this.timeline.length) + ctx.fillStyle = "#ff0000"; + else + ctx.fillStyle = "#0000ff"; + + ctx.globalAlpha = 0.1; + ctx.fillRect( + Math.min(this.timeline.length, modifiedKnob.length) * toPixels, + this.height / 2, + Math.abs(this.timeline.length - modifiedKnob.length) * toPixels, + this.timeline.lastTrackBottomY - this.height / 2 - this.y); + ctx.globalAlpha = 1; } diff --git a/src/timeline/track_meters.js b/src/timeline/track_meters.js new file mode 100644 index 0000000..3708563 --- /dev/null +++ b/src/timeline/track_meters.js @@ -0,0 +1,231 @@ +function TrackMeters(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); + this.meters = new ListByTime(); + + this.KNOB_WIDTH = 10; + this.TEXT_MAX_WIDTH = 60; +} + + +TrackMeters.prototype = new Track(); + + +TrackMeters.prototype.setSong = function(song) +{ + this.elements.clear(); + this.meters.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.meters.length; i++) + { + this._clipMeters(song.meters[i].time); + this._meterAdd(song.meters[i]); + } + + this.sanitize(); +} + + +TrackMeters.prototype.enumerateBeatsAtRange = function(timeRange, callback) +{ + var that = this; + this.meters.enumerateAffectingRange(timeRange, function (elem, start, end) + { + if (elem == null) + return; + + // TODO: Only consider beats contained in the time range. + var beatCount = 0; + for (var t = elem.meter.time; t < end; t += that.timeline.TIME_PER_WHOLE_NOTE / elem.meter.denominator) + { + callback(t, beatCount == 0); + beatCount = (beatCount + 1) % elem.meter.numerator; + } + }); +} + + +TrackMeters.prototype.sanitize = function() +{ + if (this.meters.getItemAffectingTime(0) == null) + this._meterAdd(new Meter(0, 4, 4)); +} + + +TrackMeters.prototype._meterAdd = function(meter) +{ + var elem = new Element(); + elem.track = this; + elem.meter = meter.clone(); + + this.elementRefresh(elem); + this.elements.add(elem); + this.meters.add(elem); + this.timeline.markDirtyElement(elem); +} + + +TrackMeters.prototype._clipMeters = function(time) +{ + // Check for overlapping meter changes and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingTime(time, function (elem) + { + if (elem.meter.time == time) + overlapping.push(elem); + }); + + for (var i = 0; i < overlapping.length; i++) + { + this.elements.remove(overlapping[i]); + this.meters.remove(overlapping[i]); + } +} + + +TrackMeters.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + + this._clipMeters(elem.meter.time); + this.elementRefresh(elem); + this.elements.add(elem); + this.meters.add(elem); + } + + this.sanitize(); + + this.modifiedElements = []; + this.timeline.markDirtyAll(); +} + + +TrackMeters.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + this.meters.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.meter = new Meter( + modifiedElem.time, + elem.meter.numerator, + elem.meter.denominator); + + this.modifiedElements.push(elem); +} + + +TrackMeters.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.time = elem.meter.time; + elem.timeRange = new TimeRange( + elem.meter.time - this.KNOB_WIDTH / 2 / toPixels, + elem.meter.time + (this.KNOB_WIDTH / 2 + this.TEXT_MAX_WIDTH) / toPixels); + + elem.interactKind = this.timeline.INTERACT_MOVE_TIME; + elem.interactTimeRange = new TimeRange(elem.meter.time, elem.meter.time); + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.meter.time * toPixels - this.KNOB_WIDTH / 2, + y: 0, + width: this.KNOB_WIDTH, + height: this.height + } + ]; +} + + +TrackMeters.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackMeters.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + ctx.save(); + + ctx.translate(0.5, 0.5); + + // Draw meter changes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawMeter(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawMeter(this.selectedElements[i]); + + ctx.restore(); +} + + +TrackMeters.prototype.getModifiedElement = function(elem) +{ + var time = elem.meter.time; + + if (elem.selected) + { + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + time += this.timeline.mouseMoveDeltaTime; + } + + return { + time: time + }; +} + + +TrackMeters.prototype.drawMeter = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var modifiedElem = this.getModifiedElement(elem); + + var col = "#0088ff"; + if (elem.selected) + col = "#002288"; + else if (this.timeline.hoverElement == elem) + col = "#00ccff"; + + ctx.fillStyle = col; + ctx.strokeStyle = col; + + var x = Math.floor(modifiedElem.time * toPixels); + + ctx.fillRect( + Math.floor(x - this.KNOB_WIDTH / 2) - 0.5, + 0.5, + this.KNOB_WIDTH, + this.height - 1); + + ctx.beginPath(); + ctx.moveTo(x, this.height / 2); + ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); + ctx.stroke(); + + ctx.font = "12px Verdana"; + ctx.textAlign = "start"; + ctx.textBaseline = "middle"; + + ctx.fillText( + "" + elem.meter.numerator + " / " + elem.meter.denominator, + x + 10, + this.height / 2); +} diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 4bd8ad2..7e086e5 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -1,15 +1,7 @@ function TrackNotes(timeline) { this.timeline = timeline; - - this.y = 0; - this.height = 0; - this.scrollY = 0; - this.elements = new ListByTimeRange(); - - this.selectedElements = []; - this.modifiedElements = []; } @@ -154,7 +146,7 @@ TrackNotes.prototype.redraw = function(time1, time2) var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; var scrollY = this.getModifiedScrollY(); - var xMin = Math.floor(time1 * toPixels); + var xMin = Math.floor(Math.max(0, time1) * toPixels); var xLen = Math.floor(this.timeline.length * toPixels); var xMax = Math.floor(time2 * toPixels); @@ -187,6 +179,26 @@ TrackNotes.prototype.redraw = function(time1, time2) noteHeight - 0.5); } + // Draw beat lines. + ctx.strokeStyle = "#ffffff"; + ctx.beginPath(); + this.timeline.trackMeters.enumerateBeatsAtRange(new TimeRange(time1, time2), function (time, isStrong) + { + var x = Math.floor(time * toPixels); + + ctx.moveTo(x, that.height - noteHeight * (maxPitch - minPitch)); + ctx.lineTo(x, that.height); + + if (isStrong) + { + ctx.moveTo(x - 1, that.height - noteHeight * (maxPitch - minPitch)); + ctx.lineTo(x - 1, that.height); + ctx.moveTo(x + 1, that.height - noteHeight * (maxPitch - minPitch)); + ctx.lineTo(x + 1, that.height); + } + }); + ctx.stroke(); + // Draw notes. this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) { that.drawNote(elem); }); diff --git a/src/util/list_by_time.js b/src/util/list_by_time.js index f3944fa..b6a1986 100644 --- a/src/util/list_by_time.js +++ b/src/util/list_by_time.js @@ -18,6 +18,7 @@ ListByTime.prototype.clear = function() ListByTime.prototype.add = function(item) { this.items.push(item); + this.items.sort(function (a,b) { return a.time - b.time; }); } @@ -55,9 +56,36 @@ ListByTime.prototype.enumerateOverlappingRange = function(timeRange, callback) } -ListByTime.prototype.getActiveAtTime = function(time) +ListByTime.prototype.enumerateAffectingRange = function(timeRange, callback) { - var item = null; + var lastIndex = this.getIndexAffectingTime(timeRange.start); + + for (var i = lastIndex + 1; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (this.items[i].time >= timeRange.end) + break; + + if (lastIndex == -1) + callback(null, timeRange.start, this.items[i].time); + else + callback(this.items[lastIndex], this.items[lastIndex].time, this.items[i].time); + + lastIndex = i; + } + + if (lastIndex == -1) + callback(null, timeRange.start, timeRange.end); + else + callback(this.items[lastIndex], this.items[lastIndex].time, timeRange.end); +} + + +ListByTime.prototype.getIndexAffectingTime = function(time) +{ + var index = -1; var minTimeSoFar = -1; for (var i = 0; i < this.items.length; i++) @@ -69,10 +97,23 @@ ListByTime.prototype.getActiveAtTime = function(time) if (minTimeSoFar == -1 || (itemTime < minTimeSoFar && itemTime >= time)) { - item = this.items[i]; + index = i; minTimeSoFar = itemTime; } } - return item; + if (minTimeSoFar != -1 && minTimeSoFar > time) + return -1; + + return index; +} + + +ListByTime.prototype.getItemAffectingTime = function(time) +{ + var index = this.getIndexAffectingTime(time); + if (index == -1) + return null; + else + return this.items[index]; } \ No newline at end of file From 700b75a9fa19fa4c72ca8561a770137c0ac49fab Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 27 Aug 2016 18:48:38 -0300 Subject: [PATCH 34/46] add key change track --- index.html | 3 + main.js | 13 ++- src/song/key.js | 15 +++ src/song/song.js | 7 ++ src/timeline/timeline.js | 81 +++++++------ src/timeline/track_keys.js | 226 ++++++++++++++++++++++++++++++++++++ src/timeline/track_notes.js | 71 ++++++++--- src/util/theory.js | 38 ++++++ 8 files changed, 398 insertions(+), 56 deletions(-) create mode 100644 src/song/key.js create mode 100644 src/timeline/track_keys.js create mode 100644 src/util/theory.js diff --git a/index.html b/index.html index 702851d..a0f59ed 100644 --- a/index.html +++ b/index.html @@ -14,12 +14,14 @@ + + @@ -28,6 +30,7 @@ + diff --git a/main.js b/main.js index 7012d74..2f54182 100644 --- a/main.js +++ b/main.js @@ -8,13 +8,16 @@ function main() canvas.width = div.clientWidth; var song = new Song(); + song.keyAdd(new Key(0, 0, theory.C)); + song.keyAdd(new Key(960, 0, theory.D)); + song.keyAdd(new Key(1920, 0, theory.Fs)); song.meterAdd(new Meter(0, 4, 4)); song.meterAdd(new Meter(720, 3, 4)); - song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(3 * 12 + 0))); - song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(3 * 12 + 1))); - song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(3 * 12 + 2))); - song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(3 * 12 + 3))); - song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(3 * 12 + 4))); + song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(5 * 12 + 0))); + song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(5 * 12 + 1))); + song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(5 * 12 + 2))); + song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); + song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); var timeline = new Timeline(canvas); timeline.setSong(song); diff --git a/src/song/key.js b/src/song/key.js new file mode 100644 index 0000000..5dc32bc --- /dev/null +++ b/src/song/key.js @@ -0,0 +1,15 @@ +function Key(time, scaleIndex, rootMidiPitch) +{ + this.time = time; + this.scaleIndex = scaleIndex; + this.rootMidiPitch = rootMidiPitch; +} + + +Key.prototype.clone = function() +{ + return new Key( + this.time, + this.scaleIndex, + this.rootMidiPitch); +} \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js index dc4f2f9..4f54491 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -2,6 +2,7 @@ function Song() { this.length = 960 * 4; this.notes = []; + this.keys = []; this.meters = []; } @@ -18,6 +19,12 @@ Song.prototype.noteAdd = function(note) } +Song.prototype.keyAdd = function(key) +{ + this.keys.push(key); +} + + Song.prototype.meterAdd = function(meter) { this.meters.push(meter); diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 850d88a..df3a806 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -4,9 +4,10 @@ function Timeline(canvas) // Set up constants. this.INTERACT_NONE = 0x0; - this.INTERACT_MOVE_TIME = 0x1; - this.INTERACT_MOVE_PITCH = 0x2; - this.INTERACT_STRETCH_TIME = 0x4; + this.INTERACT_SCROLL = 0x1; + this.INTERACT_MOVE_TIME = 0x2; + this.INTERACT_MOVE_PITCH = 0x4; + this.INTERACT_STRETCH_TIME = 0x8; this.TIME_PER_WHOLE_NOTE = 960; this.MAX_VALID_LENGTH = 960 * 1024; @@ -23,10 +24,11 @@ function Timeline(canvas) this.canvasHeight = 0; // Set up mouse/keyboard interaction. - this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; - window.onmousemove = function(ev) { that.handleMouseMove(ev); }; - window.onmouseup = function(ev) { that.handleMouseUp(ev); }; - //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; + this.canvas.oncontextmenu = function(ev) { that.handleContextMenu(ev); }; + this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; + window.onmousemove = function(ev) { that.handleMouseMove(ev); }; + window.onmouseup = function(ev) { that.handleMouseUp(ev); }; + //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; this.mouseDown = false; this.mouseDownPos = null; @@ -56,11 +58,13 @@ function Timeline(canvas) // Set up tracks. this.length = 0; this.trackLength = new TrackLength(this); + this.trackKeys = new TrackKeys(this); this.trackMeters = new TrackMeters(this); this.trackNotes = new TrackNotes(this); this.tracks = []; this.tracks.push(this.trackLength); + this.tracks.push(this.trackKeys); this.tracks.push(this.trackMeters); this.tracks.push(this.trackNotes); } @@ -91,6 +95,13 @@ Timeline.prototype.mouseToClient = function(ev) } +Timeline.prototype.handleContextMenu = function(ev) +{ + ev.preventDefault(); + return false; +} + + Timeline.prototype.handleMouseDown = function(ev) { var that = this; @@ -104,17 +115,24 @@ Timeline.prototype.handleMouseDown = function(ev) if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) this.unselectAll(); - if (this.hoverElement != null) + if (ev.which !== 1) + this.mouseAction = this.INTERACT_SCROLL; + + else if (this.hoverElement != null && this.hoverRegion != null) { // Handle selection of element under mouse. if (!this.hoverElement.selected) this.select(this.hoverElement); - - this.unselectAllOfDifferentKind(this.hoverElement.interactKind); - - this.mouseAction = this.hoverElement.interactKind; + + // Set mouse action to a common action of + // all selected elements. + this.mouseAction = this.hoverRegion.kind; + for (var i = 0; i < this.selectedElements.length; i++) + this.mouseAction &= this.selectedElements[i].interactKind; + this.markDirtyAllSelectedElements(0); } + else this.mouseAction = this.INTERACT_NONE; @@ -147,7 +165,7 @@ Timeline.prototype.handleMouseMove = function(ev) var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); // Handle scrolling. - if (this.mouseAction == this.INTERACT_NONE) + if (this.mouseAction == this.INTERACT_SCROLL) { var scrollTimeDelta = (this.mouseDownPos.x - mousePos.x) / this.timeToPixelsScaling; this.scrollTime = @@ -273,10 +291,10 @@ Timeline.prototype.handleMouseMove = function(ev) if (this.hoverRegion != null) regionKind = this.hoverRegion.kind; - if ((regionKind & this.INTERACT_MOVE_TIME != 0) || - (regionKind & this.INTERACT_MOVE_PITCH != 0)) + if (((regionKind & this.INTERACT_MOVE_TIME) != 0) || + ((regionKind & this.INTERACT_MOVE_PITCH) != 0)) this.canvas.style.cursor = "pointer"; - else if (regionKind & this.INTERACT_STRETCH_TIME != 0) + else if ((regionKind & this.INTERACT_STRETCH_TIME) != 0) this.canvas.style.cursor = "ew-resize" else this.canvas.style.cursor = "default"; @@ -293,7 +311,7 @@ Timeline.prototype.handleMouseUp = function(ev) // Handle releasing the mouse after dragging. if (this.mouseDown) { - if (this.mouseAction == this.INTERACT_NONE) + if (this.mouseAction == this.INTERACT_SCROLL) { if (this.mouseDownTrack != null) this.mouseDownTrack.handleScroll(); @@ -312,9 +330,9 @@ Timeline.prototype.handleMouseUp = function(ev) } } - this.mouseDown = false; - this.mouseAction = this.INTERACT_NONE; - this.mouseDownTrack = null; + this.mouseDown = false; + this.mouseAction = this.INTERACT_NONE; + this.mouseDownTrack = null; this.redraw(); } @@ -371,20 +389,6 @@ Timeline.prototype.unselectAll = function() } -Timeline.prototype.unselectAllOfDifferentKind = function(kind) -{ - for (var i = this.selectedElements.length - 1; i >= 0; i--) - { - if ((this.selectedElements[i].interactKind & kind) == 0) - { - this.selectedElements[i].unselect(); - this.markDirtyElement(this.selectedElements[i]); - this.selectedElements[i].splice(i, 1); - } - } -} - - Timeline.prototype.select = function(elem) { elem.select(); @@ -415,11 +419,14 @@ Timeline.prototype.relayout = function() this.trackLength.y = 5; this.trackLength.height = 20; - this.trackMeters.y = 25; + this.trackKeys.y = 25; + this.trackKeys.height = 20; + + this.trackMeters.y = 50; this.trackMeters.height = 20; - this.trackNotes.y = 50; - this.trackNotes.height = this.canvasHeight - 55; + this.trackNotes.y = 75; + this.trackNotes.height = this.canvasHeight - 80; this.lastTrackBottomY = this.trackNotes.y + this.trackNotes.height; diff --git a/src/timeline/track_keys.js b/src/timeline/track_keys.js new file mode 100644 index 0000000..3df28d5 --- /dev/null +++ b/src/timeline/track_keys.js @@ -0,0 +1,226 @@ +function TrackKeys(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); + this.keys = new ListByTime(); + + this.KNOB_WIDTH = 10; + this.TEXT_MAX_WIDTH = 180; +} + + +TrackKeys.prototype = new Track(); + + +TrackKeys.prototype.setSong = function(song) +{ + this.elements.clear(); + this.keys.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.keys.length; i++) + { + this._clipKeys(song.keys[i].time); + this._keyAdd(song.keys[i]); + } + + this.sanitize(); +} + + +TrackKeys.prototype.enumerateKeysAtRange = function(timeRange, callback) +{ + var that = this; + this.keys.enumerateAffectingRange(timeRange, function (elem, start, end) + { + if (elem == null) + return; + + callback(elem.key, start, end); + }); +} + + +TrackKeys.prototype.sanitize = function() +{ + if (this.keys.getItemAffectingTime(0) == null) + this._keyAdd(new Key(0, 0, theory.C)); +} + + +TrackKeys.prototype._keyAdd = function(key) +{ + var elem = new Element(); + elem.track = this; + elem.key = key.clone(); + + this.elementRefresh(elem); + this.elements.add(elem); + this.keys.add(elem); + this.timeline.markDirtyElement(elem); +} + + +TrackKeys.prototype._clipKeys = function(time) +{ + // Check for overlapping key changes and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingTime(time, function (elem) + { + if (elem.key.time == time) + overlapping.push(elem); + }); + + for (var i = 0; i < overlapping.length; i++) + { + this.elements.remove(overlapping[i]); + this.keys.remove(overlapping[i]); + } +} + + +TrackKeys.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + + this._clipKeys(elem.key.time); + this.elementRefresh(elem); + this.elements.add(elem); + this.keys.add(elem); + } + + this.sanitize(); + + this.modifiedElements = []; + this.timeline.markDirtyAll(); +} + + +TrackKeys.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + this.keys.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.key = new Key( + modifiedElem.time, + elem.key.scaleIndex, + elem.key.rootMidiPitch); + + this.modifiedElements.push(elem); +} + + +TrackKeys.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.time = elem.key.time; + elem.timeRange = new TimeRange( + elem.key.time - this.KNOB_WIDTH / 2 / toPixels, + elem.key.time + (this.KNOB_WIDTH / 2 + this.TEXT_MAX_WIDTH) / toPixels); + + elem.interactKind = this.timeline.INTERACT_MOVE_TIME; + elem.interactTimeRange = new TimeRange(elem.key.time, elem.key.time); + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.key.time * toPixels - this.KNOB_WIDTH / 2, + y: 0, + width: this.KNOB_WIDTH, + height: this.height + } + ]; +} + + +TrackKeys.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackKeys.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + ctx.save(); + + ctx.translate(0.5, 0.5); + + // Draw meter changes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawKey(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawKey(this.selectedElements[i]); + + ctx.restore(); +} + + +TrackKeys.prototype.getModifiedElement = function(elem) +{ + var time = elem.key.time; + + if (elem.selected) + { + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + time += this.timeline.mouseMoveDeltaTime; + } + + return { + time: time + }; +} + + +TrackKeys.prototype.drawKey = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var modifiedElem = this.getModifiedElement(elem); + + var col = "#ff0088"; + if (elem.selected) + col = "#880022"; + else if (this.timeline.hoverElement == elem) + col = "#ff00cc"; + + ctx.fillStyle = col; + ctx.strokeStyle = col; + + var x = Math.floor(modifiedElem.time * toPixels); + + ctx.fillRect( + Math.floor(x - this.KNOB_WIDTH / 2) - 0.5, + 0.5, + this.KNOB_WIDTH, + this.height - 1); + + ctx.beginPath(); + ctx.moveTo(x, this.height / 2); + ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); + ctx.stroke(); + + ctx.font = "12px Verdana"; + ctx.textAlign = "start"; + ctx.textBaseline = "middle"; + + ctx.fillText( + theory.pitchNameInScale(elem.key.scaleIndex, elem.key.rootMidiPitch) + + " " + theory.scales[elem.key.scaleIndex].name, + x + 10, + this.height / 2); +} diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 7e086e5..a64f21a 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -162,23 +162,56 @@ TrackNotes.prototype.redraw = function(time1, time2) ctx.translate(0, scrollY); // Draw pitch rows. - for (var i = minPitch; i <= maxPitch; i++) + this.timeline.trackKeys.enumerateKeysAtRange(new TimeRange(time1, time2), function (key, start, end) { - ctx.fillStyle = "#cccccc"; - ctx.fillRect( - xLen, - this.height - noteHeight * (i - minPitch), - xMax - xLen, - noteHeight - 0.5); + for (var i = minPitch; i <= maxPitch; i++) + { + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, i); + + switch (degree) + { + case 0: ctx.fillStyle = "#ffcccc"; break; + case 1: ctx.fillStyle = "#ffeecc"; break; + case 2: ctx.fillStyle = "#ffffcc"; break; + case 3: ctx.fillStyle = "#cceecc"; break; + case 4: ctx.fillStyle = "#ccccff"; break; + case 5: ctx.fillStyle = "#eeccff"; break; + case 6: ctx.fillStyle = "#ffccff"; break; + default: ctx.fillStyle = "#e4e4e4"; break; + } - ctx.fillStyle = "#e4e4e4"; - ctx.fillRect( - xMin, - this.height - noteHeight * (i - minPitch), - xLen - xMin, - noteHeight - 0.5); + ctx.fillRect( + start * toPixels, + that.height - noteHeight * (i - minPitch), + (end - start) * toPixels, + noteHeight - 0.5); + } + }); + + // Draw pitch rows after song length. + if (time2 > this.timeline.length) + { + for (var i = minPitch; i <= maxPitch; i++) + { + ctx.fillStyle = "#cccccc"; + ctx.fillRect( + this.timeline.length * toPixels, + this.height - noteHeight * (i - minPitch), + (time2 - this.timeline.length) * toPixels, + noteHeight - 0.5); + } } + // Mark the middle octave. + ctx.fillStyle = "#000000"; + ctx.globalAlpha = 0.1; + ctx.fillRect( + xMin, + this.height - noteHeight * (6 * 12 - minPitch), + xMax - xMin, + noteHeight * 12 - 0.5); + ctx.globalAlpha = 1; + // Draw beat lines. ctx.strokeStyle = "#ffffff"; ctx.beginPath(); @@ -199,6 +232,16 @@ TrackNotes.prototype.redraw = function(time1, time2) }); ctx.stroke(); + // Draw C-pitch row markers. + ctx.strokeStyle = "#000000"; + ctx.beginPath(); + for (var i = Math.floor(minPitch / 12) * 12; i <= maxPitch; i += 12) + { + ctx.moveTo(xMin, that.height - noteHeight * (i - minPitch)); + ctx.lineTo(xMax, that.height - noteHeight * (i - minPitch)); + } + ctx.stroke(); + // Draw notes. this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) { that.drawNote(elem); }); @@ -260,7 +303,7 @@ TrackNotes.prototype.getModifiedScrollY = function() var scrollY = this.scrollY; if (this.timeline.mouseDownTrack == this && - this.timeline.mouseAction == this.timeline.INTERACT_NONE) + this.timeline.mouseAction == this.timeline.INTERACT_SCROLL) { scrollY += this.timeline.mouseMoveScrollY; scrollY = diff --git a/src/util/theory.js b/src/util/theory.js new file mode 100644 index 0000000..c0b2887 --- /dev/null +++ b/src/util/theory.js @@ -0,0 +1,38 @@ +function Theory() +{ + var C = 0; this.C = C; + var Cs = 1; this.Cs = Cs; + var D = 2; this.D = D; + var Ds = 3; this.Ds = Ds; + var E = 4; this.E = E; + var F = 5; this.F = F; + var Fs = 6; this.Fs = Fs; + var G = 7; this.G = G; + var Gs = 8; this.Gs = Gs; + var A = 9; this.A = A; + var As = 10; this.As = As; + var B = 11; this.B = B; + + + this.scales = + [ + { name: "Major", notes: [ C, D, E, F, G, A, B ], degrees: [0, 0.5, 1, 1.5, 2, 3, 3.5, 4, 4.5, 5, 5.5, 6] } + ]; +} + + +Theory.prototype.pitchNameInScale = function(scaleIndex, midiPitch) +{ + // TODO: Take into consideration scale accidentals. + var names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; + return names[midiPitch % 12]; +} + + +Theory.prototype.pitchDegreeInKey = function(scaleIndex, rootMidiPitch, midiPitch) +{ + return this.scales[scaleIndex].degrees[(midiPitch + 12 - rootMidiPitch) % 12]; +} + + +var theory = new Theory(); \ No newline at end of file From 1c417ddeee23d0b97a47a41db171287788fb965f Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sat, 27 Aug 2016 20:16:12 -0300 Subject: [PATCH 35/46] add note coloring --- src/timeline/track_notes.js | 118 +++++++++++++++++++++++------------- src/util/list_by_time.js | 19 ++---- 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index a64f21a..0984e5b 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -170,19 +170,19 @@ TrackNotes.prototype.redraw = function(time1, time2) switch (degree) { - case 0: ctx.fillStyle = "#ffcccc"; break; - case 1: ctx.fillStyle = "#ffeecc"; break; - case 2: ctx.fillStyle = "#ffffcc"; break; - case 3: ctx.fillStyle = "#cceecc"; break; - case 4: ctx.fillStyle = "#ccccff"; break; - case 5: ctx.fillStyle = "#eeccff"; break; - case 6: ctx.fillStyle = "#ffccff"; break; + case 0: ctx.fillStyle = "#ffdddd"; break; + case 1: ctx.fillStyle = "#ffeedd"; break; + case 2: ctx.fillStyle = "#ffffdd"; break; + case 3: ctx.fillStyle = "#ddeedd"; break; + case 4: ctx.fillStyle = "#ddddff"; break; + case 5: ctx.fillStyle = "#eeddff"; break; + case 6: ctx.fillStyle = "#ffddff"; break; default: ctx.fillStyle = "#e4e4e4"; break; } ctx.fillRect( start * toPixels, - that.height - noteHeight * (i - minPitch), + that.height - noteHeight * (i - minPitch + 1), (end - start) * toPixels, noteHeight - 0.5); } @@ -202,16 +202,6 @@ TrackNotes.prototype.redraw = function(time1, time2) } } - // Mark the middle octave. - ctx.fillStyle = "#000000"; - ctx.globalAlpha = 0.1; - ctx.fillRect( - xMin, - this.height - noteHeight * (6 * 12 - minPitch), - xMax - xMin, - noteHeight * 12 - 0.5); - ctx.globalAlpha = 1; - // Draw beat lines. ctx.strokeStyle = "#ffffff"; ctx.beginPath(); @@ -232,16 +222,28 @@ TrackNotes.prototype.redraw = function(time1, time2) }); ctx.stroke(); - // Draw C-pitch row markers. - ctx.strokeStyle = "#000000"; - ctx.beginPath(); - for (var i = Math.floor(minPitch / 12) * 12; i <= maxPitch; i += 12) + // Draw root pitch markers. + this.timeline.trackKeys.enumerateKeysAtRange(new TimeRange(time1, time2), function (key, start, end) { - ctx.moveTo(xMin, that.height - noteHeight * (i - minPitch)); - ctx.lineTo(xMax, that.height - noteHeight * (i - minPitch)); - } - ctx.stroke(); - + for (var i = Math.floor(minPitch / 12) * 12 + key.rootMidiPitch; i <= maxPitch; i += 12) + { + ctx.strokeStyle = "#000000"; + ctx.beginPath(); + ctx.moveTo(start * toPixels, that.height - noteHeight * (i - minPitch)); + ctx.lineTo( end * toPixels, that.height - noteHeight * (i - minPitch)); + + if (i == 5 * 12 + key.rootMidiPitch || i == 6 * 12 + key.rootMidiPitch) + { + ctx.moveTo(start * toPixels, that.height - noteHeight * (i - minPitch) - 1); + ctx.lineTo( end * toPixels, that.height - noteHeight * (i - minPitch) - 1); + ctx.moveTo(start * toPixels, that.height - noteHeight * (i - minPitch) + 1); + ctx.lineTo( end * toPixels, that.height - noteHeight * (i - minPitch) + 1); + } + + ctx.stroke(); + } + }); + // Draw notes. this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) { that.drawNote(elem); }); @@ -288,6 +290,7 @@ TrackNotes.prototype.getModifiedElement = function(elem) return { start: start, + end: start + duration, duration: duration, pitch: pitch }; @@ -318,6 +321,7 @@ TrackNotes.prototype.getModifiedScrollY = function() TrackNotes.prototype.drawNote = function(elem) { + var that = this; var ctx = this.timeline.ctx; var toPixels = this.timeline.timeToPixelsScaling; var noteHeight = this.timeline.noteHeight; @@ -325,28 +329,60 @@ TrackNotes.prototype.drawNote = function(elem) var scrollY = this.getModifiedScrollY(); var modifiedElem = this.getModifiedElement(elem); - ctx.globalAlpha = 1; - if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) - ctx.fillStyle = "#ff8888"; - else - ctx.fillStyle = "#ff0000"; - - var x = 0.5 + Math.floor(modifiedElem.start * toPixels); - var y = 0.5 + this.height - noteHeight * (modifiedElem.pitch - minPitch); - var w = Math.floor(modifiedElem.duration * toPixels - 1); - + var y = 0.5 + that.height - noteHeight * (modifiedElem.pitch - minPitch); y = Math.max(-scrollY + 3, - Math.min(this.height - scrollY + noteHeight - 2, + Math.min(that.height - scrollY + noteHeight - 2, y)); + + this.timeline.trackKeys.enumerateKeysAtRange( + new TimeRange(modifiedElem.start, modifiedElem.end), + function (key, start, end) + { + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, modifiedElem.pitch); + + switch (degree) + { + case 0: ctx.fillStyle = "#ff0000"; break; + case 1: ctx.fillStyle = "#ff8800"; break; + case 2: ctx.fillStyle = "#ffdd00"; break; + case 3: ctx.fillStyle = "#00dd00"; break; + case 4: ctx.fillStyle = "#0000ff"; break; + case 5: ctx.fillStyle = "#8800ff"; break; + case 6: ctx.fillStyle = "#ff00ff"; break; + default: ctx.fillStyle = "#888888"; break; + } + + var isLast = end == modifiedElem.end; + var x = 0.5 + Math.floor(start * toPixels); + var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)); + + ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + }); - ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + y - noteHeight, + Math.floor(modifiedElem.duration * toPixels - 1), + noteHeight - 1); + } + if (elem.selected) { ctx.fillStyle = "#ffffff" - ctx.globalAlpha = 0.5; + ctx.globalAlpha = 0.35; - ctx.fillRect(x, y - noteHeight + 2, w, noteHeight - 1 - 4); + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + y - noteHeight + 2, + Math.floor(modifiedElem.duration * toPixels - 1), + noteHeight - 1 - 4); } + + ctx.globalAlpha = 1; } \ No newline at end of file diff --git a/src/util/list_by_time.js b/src/util/list_by_time.js index b6a1986..b2fd1d0 100644 --- a/src/util/list_by_time.js +++ b/src/util/list_by_time.js @@ -71,7 +71,7 @@ ListByTime.prototype.enumerateAffectingRange = function(timeRange, callback) if (lastIndex == -1) callback(null, timeRange.start, this.items[i].time); else - callback(this.items[lastIndex], this.items[lastIndex].time, this.items[i].time); + callback(this.items[lastIndex], Math.max(timeRange.start, this.items[lastIndex].time), this.items[i].time); lastIndex = i; } @@ -79,15 +79,12 @@ ListByTime.prototype.enumerateAffectingRange = function(timeRange, callback) if (lastIndex == -1) callback(null, timeRange.start, timeRange.end); else - callback(this.items[lastIndex], this.items[lastIndex].time, timeRange.end); + callback(this.items[lastIndex], Math.max(timeRange.start, this.items[lastIndex].time), timeRange.end); } ListByTime.prototype.getIndexAffectingTime = function(time) { - var index = -1; - var minTimeSoFar = -1; - for (var i = 0; i < this.items.length; i++) { if (this.items[i] == null) @@ -95,17 +92,11 @@ ListByTime.prototype.getIndexAffectingTime = function(time) var itemTime = this.items[i].time; - if (minTimeSoFar == -1 || (itemTime < minTimeSoFar && itemTime >= time)) - { - index = i; - minTimeSoFar = itemTime; - } + if (itemTime > time) + return i - 1; } - if (minTimeSoFar != -1 && minTimeSoFar > time) - return -1; - - return index; + return this.items.length - 1; } From 9866c54f290b47aab61581ecad50f454576c3612 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 28 Aug 2016 12:09:18 -0300 Subject: [PATCH 36/46] add striped notes; add note stretching --- index.html | 1 + src/timeline/timeline.js | 219 ++++++++++++++++++++++++----------- src/timeline/track_keys.js | 13 ++- src/timeline/track_length.js | 13 ++- src/timeline/track_meters.js | 13 ++- src/timeline/track_notes.js | 72 ++++++++---- src/util/drawing.js | 27 +++++ src/util/math.js | 10 ++ src/util/theory.js | 17 +++ src/util/timerange.js | 10 ++ 10 files changed, 299 insertions(+), 96 deletions(-) create mode 100644 src/util/drawing.js diff --git a/index.html b/index.html index a0f59ed..b1eb496 100644 --- a/index.html +++ b/index.html @@ -26,6 +26,7 @@ + diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index df3a806..b9037f1 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -3,11 +3,12 @@ function Timeline(canvas) var that = this; // Set up constants. - this.INTERACT_NONE = 0x0; - this.INTERACT_SCROLL = 0x1; - this.INTERACT_MOVE_TIME = 0x2; - this.INTERACT_MOVE_PITCH = 0x4; - this.INTERACT_STRETCH_TIME = 0x8; + this.INTERACT_NONE = 0x0; + this.INTERACT_SCROLL = 0x1; + this.INTERACT_MOVE_TIME = 0x2; + this.INTERACT_MOVE_PITCH = 0x4; + this.INTERACT_STRETCH_TIME_L = 0x8; + this.INTERACT_STRETCH_TIME_R = 0x10; this.TIME_PER_WHOLE_NOTE = 960; this.MAX_VALID_LENGTH = 960 * 1024; @@ -30,14 +31,16 @@ function Timeline(canvas) window.onmouseup = function(ev) { that.handleMouseUp(ev); }; //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; - this.mouseDown = false; - this.mouseDownPos = null; - this.mouseDownTrack = null; - this.mouseDownScrollTime = 0; - this.mouseAction = this.INTERACT_NONE; - this.mouseMoveDeltaTime = 0; - this.mouseMovePitch = 0; - this.mouseMoveScrollY = 0; + this.mouseDown = false; + this.mouseDownPos = null; + this.mouseDownTrack = null; + this.mouseDownScrollTime = 0; + this.mouseAction = this.INTERACT_NONE; + this.mouseMoveDeltaTime = 0; + this.mouseMovePitch = 0; + this.mouseMoveScrollY = 0; + this.mouseStretchTimePivot = 0; + this.mouseStretchTimeOrigin = 0; this.hoverElement = null; this.hoverRegion = null; @@ -111,40 +114,58 @@ Timeline.prototype.handleMouseDown = function(ev) var ctrl = ev.ctrlKey; var mousePos = this.mouseToClient(ev); - // Handle multi-selection with Ctrl. - if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) - this.unselectAll(); + this.mouseAction = this.INTERACT_NONE; + this.mouseDown = true; + this.mouseDownPos = mousePos; + this.mouseDownScrollTime = this.scrollTime; + this.mouseMoveDeltaTime = 0; + this.mouseMoveDeltaPitch = 0; + this.mouseMoveScrollY = 0; + this.mouseStretchTimePivot = 0; + this.mouseStretchTimeOrigin = 0; + if (ev.which !== 1) this.mouseAction = this.INTERACT_SCROLL; - - else if (this.hoverElement != null && this.hoverRegion != null) + else { - // Handle selection of element under mouse. - if (!this.hoverElement.selected) - this.select(this.hoverElement); - - // Set mouse action to a common action of - // all selected elements. - this.mouseAction = this.hoverRegion.kind; - for (var i = 0; i < this.selectedElements.length; i++) - this.mouseAction &= this.selectedElements[i].interactKind; + // Handle multi-selection with Ctrl. + if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) + this.unselectAll(); - this.markDirtyAllSelectedElements(0); + if (this.hoverElement != null && this.hoverRegion != null) + { + // Handle selection of element under mouse. + if (!this.hoverElement.selected) + this.select(this.hoverElement); + + // Set mouse action to a common action of + // all selected elements. + this.mouseAction = this.hoverRegion.kind; + for (var i = 0; i < this.selectedElements.length; i++) + this.mouseAction &= this.selectedElements[i].interactKind; + + // Redraw all selected elements to + // indicate multiple modification. + this.markDirtyAllSelectedElements(0); + + // Set up stretch values, if applicable. + if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0) + { + this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().end; + this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.start; + } + else if ((this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) + { + this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().start; + this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.end; + } + } } - - else - this.mouseAction = this.INTERACT_NONE; var trackIndex = this.getTrackIndexAtY(mousePos.y); this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); - this.mouseDown = true; - this.mouseDownPos = mousePos; - this.mouseDownScrollTime = this.scrollTime; - this.mouseMoveDeltaTime = 0; - this.mouseMoveDeltaPitch = 0; - this.mouseMoveScrollY = 0; this.redraw(); } @@ -180,25 +201,13 @@ Timeline.prototype.handleMouseMove = function(ev) this.markDirtyAll(); } - if (this.selectedElements.length != 0) + else if (this.selectedElements.length != 0) { // Handle time displacement. if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) { // Get merged time ranges of all selected elements. - var allEncompasingInteractTimeRange = null; - - for (var i = 0; i < this.selectedElements.length; i++) - { - var timeRange = this.selectedElements[i].interactTimeRange; - if (timeRange == null) - continue; - - if (allEncompasingInteractTimeRange == null) - allEncompasingInteractTimeRange = timeRange.clone(); - else - allEncompasingInteractTimeRange.merge(timeRange); - } + var allTimeRange = this.getSelectedElementsTimeRange(); // Mark elements' previous positions as dirty, // to redraw over when they move away. @@ -206,12 +215,12 @@ Timeline.prototype.handleMouseMove = function(ev) // Calculate displacement, // ensuring that elements cannot fall out of bounds. - if (allEncompasingInteractTimeRange == null) + if (allTimeRange == null) this.mouseMoveDeltaTime = mouseTimeDelta; else this.mouseMoveDeltaTime = - Math.max(-allEncompasingInteractTimeRange.start, - Math.min(this.length - allEncompasingInteractTimeRange.end, + Math.max(-allTimeRange.start, + Math.min(this.length - allTimeRange.end, mouseTimeDelta)); this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); @@ -224,6 +233,39 @@ Timeline.prototype.handleMouseMove = function(ev) else this.markDirtyAllSelectedElements(0); + // Handle time stretching. + if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0 || + (this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) + { + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they stretch away. + var prevStretchedAllTimeRange = allTimeRange.clone(); + prevStretchedAllTimeRange.stretch( + this.mouseStretchTimePivot, + this.mouseStretchTimeOrigin, + this.mouseMoveDeltaTime); + + this.markDirtyTimeRange(prevStretchedAllTimeRange); + + // FIXME: Calculate stretch, + // ensuring that elements cannot stretch out of bounds. + this.mouseMoveDeltaTime = mouseTimeDelta; + this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they stretch to. + var newStretchedAllTimeRange = allTimeRange.clone(); + newStretchedAllTimeRange.stretch( + this.mouseStretchTimePivot, + this.mouseStretchTimeOrigin, + this.mouseMoveDeltaTime); + + this.markDirtyTimeRange(newStretchedAllTimeRange); + } + // Handle pitch displacement. if ((this.mouseAction & this.INTERACT_MOVE_PITCH) != 0) { @@ -266,22 +308,24 @@ Timeline.prototype.handleMouseMove = function(ev) { var track = this.tracks[trackIndex]; - track.elements.enumerateOverlappingTime(mouseTime, function (elem) - { - for (var e = 0; e < elem.regions.length; e++) + track.elements.enumerateOverlappingRange( + new TimeRange(mouseTime - 10 * this.timeToPixelsScaling, mouseTime + 10 * this.timeToPixelsScaling), + function (elem) { - var region = elem.regions[e]; - - if (mousePos.xScrolled >= region.x && - mousePos.xScrolled <= region.x + region.width && - mousePos.y >= region.y + track.y + track.scrollY && - mousePos.y <= region.y + region.height + track.y + track.scrollY) + for (var e = 0; e < elem.regions.length; e++) { - that.hoverElement = elem; - that.hoverRegion = region; + var region = elem.regions[e]; + + if (mousePos.xScrolled >= region.x && + mousePos.xScrolled <= region.x + region.width && + mousePos.y >= region.y + track.y + track.scrollY && + mousePos.y <= region.y + region.height + track.y + track.scrollY) + { + that.hoverElement = elem; + that.hoverRegion = region; + } } - } - }); + }); } if (this.hoverElement != null) @@ -294,7 +338,8 @@ Timeline.prototype.handleMouseMove = function(ev) if (((regionKind & this.INTERACT_MOVE_TIME) != 0) || ((regionKind & this.INTERACT_MOVE_PITCH) != 0)) this.canvas.style.cursor = "pointer"; - else if ((regionKind & this.INTERACT_STRETCH_TIME) != 0) + else if ((regionKind & this.INTERACT_STRETCH_TIME_L) != 0 || + (regionKind & this.INTERACT_STRETCH_TIME_R) != 0) this.canvas.style.cursor = "ew-resize" else this.canvas.style.cursor = "default"; @@ -360,6 +405,12 @@ Timeline.prototype.markDirty = function(timeStart, timeEnd) } +Timeline.prototype.markDirtyTimeRange = function(timeRange) +{ + this.markDirty(timeRange.start, timeRange.end); +} + + Timeline.prototype.markDirtyElement = function(elem) { this.markDirty(elem.timeRange.start, elem.timeRange.end); @@ -397,6 +448,26 @@ Timeline.prototype.select = function(elem) } +Timeline.prototype.getSelectedElementsTimeRange = function() +{ + var allTimeRange = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + var timeRange = this.selectedElements[i].interactTimeRange; + if (timeRange == null) + continue; + + if (allTimeRange == null) + allTimeRange = timeRange.clone(); + else + allTimeRange.merge(timeRange); + } + + return allTimeRange; +} + + Timeline.prototype.getTrackIndexAtY = function(y) { for (var i = 0; i < this.tracks.length; i++) @@ -445,7 +516,17 @@ Timeline.prototype.redraw = function() // Return early if nothing dirty. if (this.redrawDirtyTimeMin == -1 || this.redrawDirtyTimeMax == -1) return; - + + /* + // Clear background for redraw debug. + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect( + 0, + 0, + this.canvasWidth, + this.canvasHeight); + */ + this.ctx.save(); this.ctx.translate( Math.floor(this.OFFSET_X - this.scrollTime * this.timeToPixelsScaling), diff --git a/src/timeline/track_keys.js b/src/timeline/track_keys.js index 3df28d5..3fea0cc 100644 --- a/src/timeline/track_keys.js +++ b/src/timeline/track_keys.js @@ -124,8 +124,9 @@ TrackKeys.prototype.elementRefresh = function(elem) elem.key.time - this.KNOB_WIDTH / 2 / toPixels, elem.key.time + (this.KNOB_WIDTH / 2 + this.TEXT_MAX_WIDTH) / toPixels); - elem.interactKind = this.timeline.INTERACT_MOVE_TIME; elem.interactTimeRange = new TimeRange(elem.key.time, elem.key.time); + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; elem.regions = [ { @@ -177,6 +178,16 @@ TrackKeys.prototype.getModifiedElement = function(elem) { if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) time += this.timeline.mouseMoveDeltaTime; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + time = stretch( + time, + this.timeline.mouseStretchTimePivot, + this.timeline.mouseStretchTimeOrigin, + this.timeline.mouseMoveDeltaTime); + } } return { diff --git a/src/timeline/track_length.js b/src/timeline/track_length.js index 1fee24c..b0e2a77 100644 --- a/src/timeline/track_length.js +++ b/src/timeline/track_length.js @@ -51,7 +51,8 @@ TrackLength.prototype.elementRefresh = function(elem) elem.length + this.LENGTH_KNOB_WIDTH / 2 / toPixels); elem.length = this.timeline.length; - elem.interactKind = this.timeline.INTERACT_MOVE_TIME; + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; elem.regions = [ { @@ -129,6 +130,16 @@ TrackLength.prototype.getModifiedLengthKnob = function(elem) { if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) length += this.timeline.mouseMoveDeltaTime; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + length = stretch( + length, + this.timeline.mouseStretchTimePivot, + this.timeline.mouseStretchTimeOrigin, + this.timeline.mouseMoveDeltaTime); + } } length = diff --git a/src/timeline/track_meters.js b/src/timeline/track_meters.js index 3708563..71335b9 100644 --- a/src/timeline/track_meters.js +++ b/src/timeline/track_meters.js @@ -130,8 +130,9 @@ TrackMeters.prototype.elementRefresh = function(elem) elem.meter.time - this.KNOB_WIDTH / 2 / toPixels, elem.meter.time + (this.KNOB_WIDTH / 2 + this.TEXT_MAX_WIDTH) / toPixels); - elem.interactKind = this.timeline.INTERACT_MOVE_TIME; elem.interactTimeRange = new TimeRange(elem.meter.time, elem.meter.time); + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; elem.regions = [ { @@ -183,6 +184,16 @@ TrackMeters.prototype.getModifiedElement = function(elem) { if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) time += this.timeline.mouseMoveDeltaTime; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + time = stretch( + time, + this.timeline.mouseStretchTimePivot, + this.timeline.mouseStretchTimeOrigin, + this.timeline.mouseMoveDeltaTime); + } } return { diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 0984e5b..0ec9531 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -111,9 +111,11 @@ TrackNotes.prototype.elementRefresh = function(elem) var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; elem.timeRange = elem.note.timeRange.clone(); - elem.interactKind = this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH; elem.interactTimeRange = elem.note.timeRange.clone(); elem.interactPitch = elem.note.pitch.clone(); + elem.interactKind = + this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; elem.regions = [ { @@ -122,6 +124,20 @@ TrackNotes.prototype.elementRefresh = function(elem) y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, width: elem.note.timeRange.duration() * toPixels, height: noteHeight - 1 + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_L, + x: elem.note.timeRange.start * toPixels - 4, + y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, + width: 4, + height: noteHeight - 1 + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_R, + x: elem.note.timeRange.end * toPixels, + y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, + width: 4, + height: noteHeight - 1 } ]; } @@ -275,23 +291,34 @@ TrackNotes.prototype.redraw = function(time1, time2) TrackNotes.prototype.getModifiedElement = function(elem) { - var start = elem.note.timeRange.start; - var duration = elem.note.timeRange.duration(); - var pitch = elem.note.pitch.midiPitch; + var timeRange = elem.note.timeRange.clone(); + var pitch = elem.note.pitch.midiPitch; if (elem.selected) { if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) - start += this.timeline.mouseMoveDeltaTime; + { + timeRange.start += this.timeline.mouseMoveDeltaTime; + timeRange.end += this.timeline.mouseMoveDeltaTime; + } if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) pitch += this.timeline.mouseMoveDeltaPitch; + + if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + timeRange.stretch( + this.timeline.mouseStretchTimePivot, + this.timeline.mouseStretchTimeOrigin, + this.timeline.mouseMoveDeltaTime); + } } return { - start: start, - end: start + duration, - duration: duration, + start: timeRange.start, + end: timeRange.end, + duration: timeRange.duration(), pitch: pitch }; } @@ -339,25 +366,22 @@ TrackNotes.prototype.drawNote = function(elem) new TimeRange(modifiedElem.start, modifiedElem.end), function (key, start, end) { - var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, modifiedElem.pitch); - - switch (degree) - { - case 0: ctx.fillStyle = "#ff0000"; break; - case 1: ctx.fillStyle = "#ff8800"; break; - case 2: ctx.fillStyle = "#ffdd00"; break; - case 3: ctx.fillStyle = "#00dd00"; break; - case 4: ctx.fillStyle = "#0000ff"; break; - case 5: ctx.fillStyle = "#8800ff"; break; - case 6: ctx.fillStyle = "#ff00ff"; break; - default: ctx.fillStyle = "#888888"; break; - } - var isLast = end == modifiedElem.end; var x = 0.5 + Math.floor(start * toPixels); - var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)); + var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)) - ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, modifiedElem.pitch); + if (Math.floor(degree) == degree) + { + ctx.fillStyle = theory.degreeColor(degree); + ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + } + else + { + var color1 = theory.degreeColor(Math.floor(degree)); + var color2 = theory.degreeColor(Math.ceil(degree)); + drawStripedRect(ctx, x, y - noteHeight, w, noteHeight - 1, color1, color2); + } }); if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) diff --git a/src/util/drawing.js b/src/util/drawing.js new file mode 100644 index 0000000..a84d496 --- /dev/null +++ b/src/util/drawing.js @@ -0,0 +1,27 @@ +function drawStripedRect(ctx, x, y, w, h, color1, color2) +{ + ctx.save(); + + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + ctx.fillStyle = color1; + ctx.fillRect(x, y, w, h); + + ctx.strokeStyle = color2; + ctx.lineWidth = 4; + ctx.beginPath(); + + var lineX = 0; + while (lineX - h * 0.6 < w) + { + ctx.moveTo(x + lineX + 5, y - 5); + ctx.lineTo(x + lineX - h * 0.6 - 5, y + h + 5); + lineX += 10; + } + + ctx.stroke(); + + ctx.restore(); +} \ No newline at end of file diff --git a/src/util/math.js b/src/util/math.js index a044a52..d10fb19 100644 --- a/src/util/math.js +++ b/src/util/math.js @@ -1,4 +1,14 @@ function snap(x, step) { return Math.round(x / step) * step; +} + + +function stretch(x, pivot, origin, delta) +{ + var dist = (origin - pivot); + var p = (x - pivot) / dist; + var move = (origin + delta - pivot) / dist; + + return Math.round(pivot + dist * (p * move)); } \ No newline at end of file diff --git a/src/util/theory.js b/src/util/theory.js index c0b2887..5965264 100644 --- a/src/util/theory.js +++ b/src/util/theory.js @@ -35,4 +35,21 @@ Theory.prototype.pitchDegreeInKey = function(scaleIndex, rootMidiPitch, midiPitc } +Theory.prototype.degreeColor = function(degree) +{ + switch (degree) + { + case 0: return "#ff0000"; + case 1: return "#ff8800"; + case 2: return "#ffdd00"; + case 3: return "#00dd00"; + case 4: return "#0000ff"; + case 5: return "#8800ff"; + case 6: return "#ff00ff"; + case 7: return "#ff0000"; + default: return "#888888"; + } +} + + var theory = new Theory(); \ No newline at end of file diff --git a/src/util/timerange.js b/src/util/timerange.js index 0202b6f..2c78c15 100644 --- a/src/util/timerange.js +++ b/src/util/timerange.js @@ -18,6 +18,16 @@ TimeRange.prototype.merge = function(other) } +TimeRange.prototype.stretch = function(pivot, origin, delta) +{ + var start = stretch(this.start, pivot, origin, delta); + var end = stretch(this.end, pivot, origin, delta); + + this.start = Math.min(start, end); + this.end = Math.max(start, end); +} + + TimeRange.prototype.getClippedParts = function(clipRange) { var parts = []; From 8aa25696a99df797839bf0f20c49bdce01bff0e5 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Sun, 28 Aug 2016 16:00:45 -0300 Subject: [PATCH 37/46] add chord track --- index.html | 2 + main.js | 4 + src/song/chord.js | 12 ++ src/song/song.js | 7 + src/timeline/timeline.js | 9 +- src/timeline/track_chords.js | 367 +++++++++++++++++++++++++++++++++++ src/util/theory.js | 31 ++- 7 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 src/song/chord.js create mode 100644 src/timeline/track_chords.js diff --git a/index.html b/index.html index b1eb496..3805dee 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,7 @@ + @@ -21,6 +22,7 @@ + diff --git a/main.js b/main.js index 2f54182..df3c99f 100644 --- a/main.js +++ b/main.js @@ -18,6 +18,10 @@ function main() song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(5 * 12 + 2))); song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); + song.chordAdd(new Chord(new TimeRange( 0, 240), 0, theory.C)); + song.chordAdd(new Chord(new TimeRange(240, 480), 1, theory.D)); + song.chordAdd(new Chord(new TimeRange(480, 720), 2, theory.Fs)); + song.chordAdd(new Chord(new TimeRange(720, 960), 3, theory.As)); var timeline = new Timeline(canvas); timeline.setSong(song); diff --git a/src/song/chord.js b/src/song/chord.js new file mode 100644 index 0000000..584847f --- /dev/null +++ b/src/song/chord.js @@ -0,0 +1,12 @@ +function Chord(timeRange, chordIndex, rootMidiPitch) +{ + this.timeRange = timeRange; + this.chordIndex = chordIndex; + this.rootMidiPitch = rootMidiPitch; +} + + +Chord.prototype.clone = function() +{ + return new Chord(this.timeRange.clone(), this.chordIndex, this.rootMidiPitch); +} \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js index 4f54491..2fd54d2 100644 --- a/src/song/song.js +++ b/src/song/song.js @@ -2,6 +2,7 @@ function Song() { this.length = 960 * 4; this.notes = []; + this.chords = []; this.keys = []; this.meters = []; } @@ -19,6 +20,12 @@ Song.prototype.noteAdd = function(note) } +Song.prototype.chordAdd = function(chord) +{ + this.chords.push(chord); +} + + Song.prototype.keyAdd = function(key) { this.keys.push(key); diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index b9037f1..3bf2cc9 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -64,12 +64,14 @@ function Timeline(canvas) this.trackKeys = new TrackKeys(this); this.trackMeters = new TrackMeters(this); this.trackNotes = new TrackNotes(this); + this.trackChords = new TrackChords(this); this.tracks = []; this.tracks.push(this.trackLength); this.tracks.push(this.trackKeys); this.tracks.push(this.trackMeters); this.tracks.push(this.trackNotes); + this.tracks.push(this.trackChords); } @@ -497,9 +499,12 @@ Timeline.prototype.relayout = function() this.trackMeters.height = 20; this.trackNotes.y = 75; - this.trackNotes.height = this.canvasHeight - 80; + this.trackNotes.height = this.canvasHeight - 80 - 65; - this.lastTrackBottomY = this.trackNotes.y + this.trackNotes.height; + this.trackChords.y = this.canvasHeight - 60; + this.trackChords.height = 55; + + this.lastTrackBottomY = this.trackChords.y + this.trackChords.height; for (var i = 0; i < this.tracks.length; i++) this.tracks[i].relayout(); diff --git a/src/timeline/track_chords.js b/src/timeline/track_chords.js new file mode 100644 index 0000000..d3b7e21 --- /dev/null +++ b/src/timeline/track_chords.js @@ -0,0 +1,367 @@ +function TrackChords(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); +} + + +TrackChords.prototype = new Track(); + + +TrackChords.prototype.setSong = function(song) +{ + this.elements.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.chords.length; i++) + { + this._clipChords(song.chords[i].timeRange); + this._chordAdd(song.chords[i]); + } +} + + +TrackChords.prototype._chordAdd = function(chord) +{ + var elem = new Element(); + elem.track = this; + elem.chord = chord.clone(); + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); +} + + +TrackChords.prototype._clipChords = function(timeRange) +{ + // Check for overlapping chords and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingRange(timeRange, function (elem) + { overlapping.push(elem); }); + + for (var i = 0; i < overlapping.length; i++) + this.elements.remove(overlapping[i]); + + for (var i = 0; i < overlapping.length; i++) + { + var elem = overlapping[i]; + + var parts = elem.chord.timeRange.getClippedParts(timeRange); + for (var p = 0; p < parts.length; p++) + { + var clippedChord = elem.chord.clone(); + clippedChord.timeRange = parts[p]; + this._chordAdd(clippedChord); + } + } +} + + +TrackChords.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + this._clipChords(elem.chord.timeRange); + } + + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); + } + + this.modifiedElements = []; +} + + +TrackChords.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.chord = elem.chord.clone(); + elem.chord.timeRange = + new TimeRange(modifiedElem.start, modifiedElem.start + modifiedElem.duration); + + this.modifiedElements.push(elem); +} + + +TrackChords.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.timeRange = elem.chord.timeRange.clone(); + elem.interactTimeRange = elem.chord.timeRange.clone(); + elem.interactKind = + this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.chord.timeRange.start * toPixels, + y: 0, + width: elem.chord.timeRange.duration() * toPixels, + height: this.height + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_L, + x: elem.chord.timeRange.start * toPixels - 4, + y: 0, + width: 4, + height: this.height + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_R, + x: elem.chord.timeRange.end * toPixels, + y: 0, + width: 4, + height: this.height + } + ]; +} + + +TrackChords.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackChords.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var xMin = Math.floor(Math.max(0, time1) * toPixels); + var xLen = Math.floor(this.timeline.length * toPixels); + var xMax = Math.floor(time2 * toPixels); + + ctx.save(); + + ctx.beginPath(); + ctx.rect(xMin - 10, 0, xMax - xMin + 20, this.height + 1); + ctx.clip(); + + ctx.translate(0.5, 0.5); + + // Draw background. + ctx.fillStyle = "#e4e4e4"; + ctx.fillRect( + xMin, + 0, + xMax, + this.height); + + // Draw background after song length. + if (time2 > this.timeline.length) + { + ctx.fillStyle = "#cccccc"; + ctx.fillRect( + this.timeline.length * toPixels, + 0, + (time2 - this.timeline.length) * toPixels, + this.height); + } + + // Draw beat lines. + ctx.strokeStyle = "#ffffff"; + ctx.beginPath(); + this.timeline.trackMeters.enumerateBeatsAtRange(new TimeRange(time1, time2), function (time, isStrong) + { + var x = Math.floor(time * toPixels); + + ctx.moveTo(x, 0); + ctx.lineTo(x, that.height); + + if (isStrong) + { + ctx.moveTo(x - 1, 0); + ctx.lineTo(x - 1, that.height); + ctx.moveTo(x + 1, 0); + ctx.lineTo(x + 1, that.height); + } + }); + ctx.stroke(); + + // Draw notes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawChord(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawChord(this.selectedElements[i]); + + // Draw borders. + ctx.strokeStyle = "#000000"; + ctx.beginPath(); + ctx.moveTo(xMin, 0); + ctx.lineTo(xMax, 0); + + ctx.moveTo(xMin, this.height); + ctx.lineTo(xMax, this.height); + + ctx.moveTo(0, 0); + ctx.lineTo(0, this.height); + + ctx.moveTo(xLen, 0); + ctx.lineTo(xLen, this.height); + ctx.stroke(); + + ctx.restore(); +} + + +TrackChords.prototype.getModifiedElement = function(elem) +{ + var timeRange = elem.chord.timeRange.clone(); + + if (elem.selected) + { + if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + { + timeRange.start += this.timeline.mouseMoveDeltaTime; + timeRange.end += this.timeline.mouseMoveDeltaTime; + } + + if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + timeRange.stretch( + this.timeline.mouseStretchTimePivot, + this.timeline.mouseStretchTimeOrigin, + this.timeline.mouseMoveDeltaTime); + } + } + + return { + start: timeRange.start, + end: timeRange.end, + duration: timeRange.duration() + }; +} + + +TrackChords.prototype.drawChord = function(elem) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var modifiedElem = this.getModifiedElement(elem); + + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.75; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + 0, + Math.floor(modifiedElem.duration * toPixels - 1), + this.height); + + this.timeline.trackKeys.enumerateKeysAtRange( + new TimeRange(modifiedElem.start, modifiedElem.end), + function (key, start, end) + { + var isLast = end == modifiedElem.end; + var x = 0.5 + Math.floor(start * toPixels); + var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)) + + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, elem.chord.rootMidiPitch); + if (Math.floor(degree) == degree) + { + ctx.fillStyle = theory.degreeColor(degree); + ctx.fillRect(x, 0.5, w, 5); + ctx.fillRect(x, that.height - 5, w, 5); + } + else + { + var color1 = theory.degreeColor(Math.floor(degree)); + var color2 = theory.degreeColor(Math.ceil(degree)); + drawStripedRect(ctx, x, 0.5, w, 5, color1, color2); + drawStripedRect(ctx, x, that.height - 5, w, 5, color1, color2); + } + + // Draw roman symbol. + var mainSymbol = theory.chordSymbolInKey( + key.scaleIndex, key.rootMidiPitch, elem.chord.rootMidiPitch, + theory.chords[elem.chord.chordIndex].uppercase); + + ctx.fillStyle = "#000000"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + + ctx.font = "20px Tahoma"; + var supTextWidth = ctx.measureText(theory.chords[elem.chord.chordIndex].symbolSup).width; + var subTextWidth = ctx.measureText(theory.chords[elem.chord.chordIndex].symbolSub).width; + + ctx.font = "30px Tahoma"; + var mainTextWidth = ctx.measureText(mainSymbol).width; + var totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; + + var maxTextWidth = w - 2; + if (totalTextWidth > maxTextWidth) + { + var proportion = totalTextWidth / maxTextWidth; + supTextWidth /= proportion; + subTextWidth /= proportion; + mainTextWidth /= proportion; + totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; + } + + ctx.fillText( + mainSymbol, + x + w / 2 - totalTextWidth / 2 + mainTextWidth / 2, + that.height / 2, + maxTextWidth - supTextWidth - subTextWidth); + + ctx.font = "20px Tahoma"; + ctx.fillText( + theory.chords[elem.chord.chordIndex].symbolSup, + x + w / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, + that.height / 2 - 10, + maxTextWidth - mainTextWidth - subTextWidth); + + ctx.fillText( + theory.chords[elem.chord.chordIndex].symbolSub, + x + w / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth + subTextWidth / 2, + that.height / 2 + 10, + maxTextWidth - mainTextWidth - supTextWidth); + }); + + if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + 0, + Math.floor(modifiedElem.duration * toPixels - 1), + this.height); + } + + if (elem.selected) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + 0, + Math.floor(modifiedElem.duration * toPixels - 1), + this.height); + } + + ctx.globalAlpha = 1; +} \ No newline at end of file diff --git a/src/util/theory.js b/src/util/theory.js index 5965264..6bf5933 100644 --- a/src/util/theory.js +++ b/src/util/theory.js @@ -18,13 +18,33 @@ function Theory() [ { name: "Major", notes: [ C, D, E, F, G, A, B ], degrees: [0, 0.5, 1, 1.5, 2, 3, 3.5, 4, 4.5, 5, 5.5, 6] } ]; + + + this.chords = + [ + { notes: [ C, E, G ], uppercase: true, symbolSup: "", symbolSub: "", name: "Major" }, + { notes: [ C, Ds, G ], uppercase: false, symbolSup: "", symbolSub: "", name: "Minor" }, + { notes: [ C, Ds, Fs ], uppercase: false, symbolSup: "o", symbolSub: "", name: "Diminished" }, + { notes: [ C, E, Gs ], uppercase: true, symbolSup: "+", symbolSub: "", name: "Augmented" }, + { notes: [ C, E, Fs ], uppercase: true, symbolSup: "(♭5)", symbolSub: "", name: "Flat Fifth(?)" }, + { notes: [ C, D, Fs ], uppercase: true, symbolSup: "(?)", symbolSub: "", name: "(?)" }, + + { notes: [ C, E, G, As ], uppercase: true, symbolSup: "7", symbolSub: "", name: "Dominant Seventh" }, + { notes: [ C, E, G, B ], uppercase: true, symbolSup: "M7", symbolSub: "", name: "Major Seventh" }, + { notes: [ C, Ds, G, As ], uppercase: false, symbolSup: "7", symbolSub: "", name: "Minor Seventh" }, + { notes: [ C, Ds, G, B ], uppercase: true, symbolSup: "m(M7)", symbolSub: "", name: "Minor-Major Seventh" }, + { notes: [ C, Ds, G, A ], uppercase: false, symbolSup: "o7", symbolSub: "", name: "Diminished Seventh" }, + { notes: [ C, Ds, Fs, As ], uppercase: false, symbolSup: "ø7", symbolSub: "", name: "Half-Diminished Seventh" }, + { notes: [ C, E, Gs, As ], uppercase: true, symbolSup: "+7", symbolSub: "", name: "Augmented Seventh" }, + { notes: [ C, E, Gs, B ], uppercase: true, symbolSup: "+(M7)", symbolSub: "", name: "Augmented Major Seventh" } + ]; } Theory.prototype.pitchNameInScale = function(scaleIndex, midiPitch) { // TODO: Take into consideration scale accidentals. - var names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; + var names = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; return names[midiPitch % 12]; } @@ -35,6 +55,15 @@ Theory.prototype.pitchDegreeInKey = function(scaleIndex, rootMidiPitch, midiPitc } +Theory.prototype.chordSymbolInKey = function(scaleIndex, rootMidiPitch, midiPitch, uppercase) +{ + // TODO: Take into consideration scale accidentals. + var upper = ["I", "♭II", "II", "♭III", "III", "IV", "♭V", "V", "♭VI", "VI", "♭VII", "VII"]; + var lower = ["i", "♭ii", "ii", "♭iii", "iii", "iv", "♭v", "v", "♭vi", "vi", "♭vii", "vii"]; + return (uppercase ? upper : lower)[(midiPitch + 12 - rootMidiPitch) % 12]; +} + + Theory.prototype.degreeColor = function(degree) { switch (degree) From 1bc2451655e0d17716d7f91cb1e5add3161a6a95 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Mon, 29 Aug 2016 09:09:13 -0300 Subject: [PATCH 38/46] add cursor --- main.js | 6 +- src/timeline/timeline.js | 155 ++++++++++++++++++++++++++++++++++++--- src/util/theory.js | 14 +++- 3 files changed, 162 insertions(+), 13 deletions(-) diff --git a/main.js b/main.js index df3c99f..3532ae3 100644 --- a/main.js +++ b/main.js @@ -19,9 +19,9 @@ function main() song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); song.chordAdd(new Chord(new TimeRange( 0, 240), 0, theory.C)); - song.chordAdd(new Chord(new TimeRange(240, 480), 1, theory.D)); - song.chordAdd(new Chord(new TimeRange(480, 720), 2, theory.Fs)); - song.chordAdd(new Chord(new TimeRange(720, 960), 3, theory.As)); + song.chordAdd(new Chord(new TimeRange(240, 480), 5, theory.D)); + song.chordAdd(new Chord(new TimeRange(480, 720), 10, theory.Fs)); + song.chordAdd(new Chord(new TimeRange(720, 960), 15, theory.As)); var timeline = new Timeline(canvas); timeline.setSong(song); diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 3bf2cc9..db4c3d0 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -3,12 +3,13 @@ function Timeline(canvas) var that = this; // Set up constants. - this.INTERACT_NONE = 0x0; - this.INTERACT_SCROLL = 0x1; - this.INTERACT_MOVE_TIME = 0x2; - this.INTERACT_MOVE_PITCH = 0x4; - this.INTERACT_STRETCH_TIME_L = 0x8; - this.INTERACT_STRETCH_TIME_R = 0x10; + this.INTERACT_NONE = 0x00; + this.INTERACT_SCROLL = 0x01; + this.INTERACT_CURSOR = 0x02; + this.INTERACT_MOVE_TIME = 0x04; + this.INTERACT_MOVE_PITCH = 0x08; + this.INTERACT_STRETCH_TIME_L = 0x10; + this.INTERACT_STRETCH_TIME_R = 0x20; this.TIME_PER_WHOLE_NOTE = 960; this.MAX_VALID_LENGTH = 960 * 1024; @@ -41,6 +42,12 @@ function Timeline(canvas) this.mouseMoveScrollY = 0; this.mouseStretchTimePivot = 0; this.mouseStretchTimeOrigin = 0; + + this.cursorVisible = false; + this.cursorTime1 = 0; + this.cursorTrack1 = 0; + this.cursorTime2 = 0; + this.cursorTrack2 = 0; this.hoverElement = null; this.hoverRegion = null; @@ -88,6 +95,37 @@ Timeline.prototype.setSong = function(song) } +Timeline.prototype.setCursor = function(time, trackIndex) +{ + time = + Math.max(0, + Math.min(this.length, + time)); + + this.cursorTime1 = time; + this.cursorTime2 = time; + this.cursorTrack1 = trackIndex; + this.cursorTrack2 = trackIndex; + + if (trackIndex == 0) + this.cursorTrack2 = this.tracks.length - 1; +} + + +Timeline.prototype.setCursor2 = function(time, trackIndex) +{ + time = + Math.max(0, + Math.min(this.length, + time)); + + this.cursorTime2 = time; + + if (this.cursorTrack1 != 0) + this.cursorTrack2 = trackIndex; +} + + Timeline.prototype.mouseToClient = function(ev) { var rect = this.canvas.getBoundingClientRect(); @@ -113,8 +151,9 @@ Timeline.prototype.handleMouseDown = function(ev) ev.preventDefault(); - var ctrl = ev.ctrlKey; - var mousePos = this.mouseToClient(ev); + var ctrl = ev.ctrlKey; + var mousePos = this.mouseToClient(ev); + var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); this.mouseAction = this.INTERACT_NONE; this.mouseDown = true; @@ -126,6 +165,12 @@ Timeline.prototype.handleMouseDown = function(ev) this.mouseStretchTimePivot = 0; this.mouseStretchTimeOrigin = 0; + if (this.cursorVisible) + { + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + } if (ev.which !== 1) this.mouseAction = this.INTERACT_SCROLL; @@ -137,6 +182,8 @@ Timeline.prototype.handleMouseDown = function(ev) if (this.hoverElement != null && this.hoverRegion != null) { + this.cursorVisible = false; + // Handle selection of element under mouse. if (!this.hoverElement.selected) this.select(this.hoverElement); @@ -163,6 +210,19 @@ Timeline.prototype.handleMouseDown = function(ev) this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.end; } } + + else + { + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + { + this.mouseAction = this.INTERACT_CURSOR; + this.cursorVisible = true; + + this.setCursor(mouseTime, trackIndex); + this.markDirtyPixels(mouseTime, 5); + } + } } var trackIndex = this.getTrackIndexAtY(mousePos.y); @@ -202,6 +262,25 @@ Timeline.prototype.handleMouseMove = function(ev) this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); this.markDirtyAll(); } + + // Handle cursor selection. + else if (this.mouseAction == this.INTERACT_CURSOR) + { + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirtyPixels(mouseTime, 5); + this.markDirty(this.cursorTime2, mouseTime); + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + { + this.setCursor2(mouseTime, trackIndex); + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + } + else + this.setCursor2(mouseTime, this.cursorTrack1); + } else if (this.selectedElements.length != 0) { @@ -413,6 +492,14 @@ Timeline.prototype.markDirtyTimeRange = function(timeRange) } +Timeline.prototype.markDirtyPixels = function(time, pixelMargin) +{ + this.markDirty( + time - pixelMargin / this.timeToPixelsScaling, + time + pixelMargin / this.timeToPixelsScaling); +} + + Timeline.prototype.markDirtyElement = function(elem) { this.markDirty(elem.timeRange.start, elem.timeRange.end); @@ -564,7 +651,57 @@ Timeline.prototype.redraw = function() this.ctx.restore(); } - + + // Draw cursor. + if (this.cursorVisible) + { + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + + var x1 = Math.floor(time1 * this.timeToPixelsScaling) + 0.5; + var x2 = Math.floor(time2 * this.timeToPixelsScaling) + 0.5; + var y1 = this.tracks[track1].y; + var y2 = this.tracks[track2].y + this.tracks[track2].height; + + this.ctx.strokeStyle = "#0000ff"; + this.ctx.fillStyle = "#0000ff"; + + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x1, y2); + + this.ctx.moveTo(x2, y1); + this.ctx.lineTo(x2, y2); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x1 - 4, y1 - 4); + this.ctx.lineTo(x1 + 4, y1 - 4); + this.ctx.lineTo(x1, y1); + + this.ctx.moveTo(x2, y1); + this.ctx.lineTo(x2 - 4, y1 - 4); + this.ctx.lineTo(x2 + 4, y1 - 4); + this.ctx.lineTo(x2, y1); + + this.ctx.moveTo(x1, y2); + this.ctx.lineTo(x1 - 4, y2 + 4); + this.ctx.lineTo(x1 + 4, y2 + 4); + this.ctx.lineTo(x1, y2); + + this.ctx.moveTo(x2, y2); + this.ctx.lineTo(x2 - 4, y2 + 4); + this.ctx.lineTo(x2 + 4, y2 + 4); + this.ctx.lineTo(x2, y2); + this.ctx.fill(); + + this.ctx.globalAlpha = 0.25; + this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); + } + this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; this.ctx.restore(); diff --git a/src/util/theory.js b/src/util/theory.js index 6bf5933..c9d08d6 100644 --- a/src/util/theory.js +++ b/src/util/theory.js @@ -29,6 +29,12 @@ function Theory() { notes: [ C, E, Fs ], uppercase: true, symbolSup: "(♭5)", symbolSub: "", name: "Flat Fifth(?)" }, { notes: [ C, D, Fs ], uppercase: true, symbolSup: "(?)", symbolSub: "", name: "(?)" }, + { notes: [ C, D, G ], uppercase: true, symbolSup: "", symbolSub: "sus2", name: "Major with Suspended Second" }, + { notes: [ C, F, G ], uppercase: true, symbolSup: "", symbolSub: "sus4", name: "Major with Suspended Fourth" }, + { notes: [ C, E, G, A ], uppercase: true, symbolSup: "add6", symbolSub: "", name: "Major with Added Sixth" }, + { notes: [ C, E, G, D ], uppercase: true, symbolSup: "add9", symbolSub: "", name: "Major with Added Ninth" }, + { notes: [ C, E, G, F ], uppercase: true, symbolSup: "add11", symbolSub: "", name: "Major with Added Eleventh" }, + { notes: [ C, E, G, As ], uppercase: true, symbolSup: "7", symbolSub: "", name: "Dominant Seventh" }, { notes: [ C, E, G, B ], uppercase: true, symbolSup: "M7", symbolSub: "", name: "Major Seventh" }, { notes: [ C, Ds, G, As ], uppercase: false, symbolSup: "7", symbolSub: "", name: "Minor Seventh" }, @@ -36,7 +42,13 @@ function Theory() { notes: [ C, Ds, G, A ], uppercase: false, symbolSup: "o7", symbolSub: "", name: "Diminished Seventh" }, { notes: [ C, Ds, Fs, As ], uppercase: false, symbolSup: "ø7", symbolSub: "", name: "Half-Diminished Seventh" }, { notes: [ C, E, Gs, As ], uppercase: true, symbolSup: "+7", symbolSub: "", name: "Augmented Seventh" }, - { notes: [ C, E, Gs, B ], uppercase: true, symbolSup: "+(M7)", symbolSub: "", name: "Augmented Major Seventh" } + { notes: [ C, E, Gs, B ], uppercase: true, symbolSup: "+(M7)", symbolSub: "", name: "Augmented Major Seventh" }, + + { notes: [ C, E, G, B, D ], uppercase: true, symbolSup: "maj9", symbolSub: "", name: "Major Ninth" }, + { notes: [ C, Ds, G, As, D ], uppercase: true, symbolSup: "m9", symbolSub: "", name: "Minor Ninth" }, + { notes: [ C, E, G, As, D ], uppercase: true, symbolSup: "9", symbolSub: "", name: "Dominant Ninth" }, + { notes: [ C, E, G, As, Cs ], uppercase: true, symbolSup: "7♭9", symbolSub: "", name: "Dominant Minor Ninth" }, + { notes: [ C, E, G, A, D ], uppercase: true, symbolSup: "6/9", symbolSub: "", name: "6/9" }, ]; } From 2eb91d463b89178a62fbc17662eb9866032af591 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Mon, 29 Aug 2016 11:56:54 -0300 Subject: [PATCH 39/46] add element selection by cursor; add last time cursor snap by double-clicking; add keyboard events --- index.html | 50 ++-- main.js | 8 +- src/timeline/timeline.js | 407 +++++------------------------- src/timeline/timeline_keyboard.js | 44 ++++ src/timeline/timeline_mouse.js | 363 ++++++++++++++++++++++++++ src/util/timerange.js | 6 + 6 files changed, 513 insertions(+), 365 deletions(-) create mode 100644 src/timeline/timeline_keyboard.js create mode 100644 src/timeline/timeline_mouse.js diff --git a/index.html b/index.html index 3805dee..e1eed4c 100644 --- a/index.html +++ b/index.html @@ -7,33 +7,35 @@ - -
+ +
- - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main.js b/main.js index 3532ae3..22fb8ad 100644 --- a/main.js +++ b/main.js @@ -18,10 +18,10 @@ function main() song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(5 * 12 + 2))); song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); - song.chordAdd(new Chord(new TimeRange( 0, 240), 0, theory.C)); - song.chordAdd(new Chord(new TimeRange(240, 480), 5, theory.D)); - song.chordAdd(new Chord(new TimeRange(480, 720), 10, theory.Fs)); - song.chordAdd(new Chord(new TimeRange(720, 960), 15, theory.As)); + song.chordAdd(new Chord(new TimeRange(960 * 0, 960 * 1), 0, theory.C)); + song.chordAdd(new Chord(new TimeRange(960 * 1, 960 * 2), 3, theory.D)); + song.chordAdd(new Chord(new TimeRange(960 * 2, 960 * 3), 16, theory.Fs)); + song.chordAdd(new Chord(new TimeRange(960 * 3, 960 * 4), 22, theory.As)); var timeline = new Timeline(canvas); timeline.setSong(song); diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index db4c3d0..b3736c0 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -30,8 +30,9 @@ function Timeline(canvas) this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; window.onmousemove = function(ev) { that.handleMouseMove(ev); }; window.onmouseup = function(ev) { that.handleMouseUp(ev); }; - //window.onkeydown = function(ev) { that.handleKeyDown(ev); }; + window.onkeydown = function(ev) { that.handleKeyDown(ev); }; + this.mouseDownDate = new Date(); this.mouseDown = false; this.mouseDownPos = null; this.mouseDownTrack = null; @@ -97,6 +98,10 @@ Timeline.prototype.setSong = function(song) Timeline.prototype.setCursor = function(time, trackIndex) { + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + time = Math.max(0, Math.min(this.length, @@ -109,357 +114,62 @@ Timeline.prototype.setCursor = function(time, trackIndex) if (trackIndex == 0) this.cursorTrack2 = this.tracks.length - 1; + + this.markDirtyPixels(this.cursorTime1, 5); +} + + +Timeline.prototype.setCursorBoth = function(time1, time2, track1, track2) +{ + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + time1 = + Math.max(0, + Math.min(this.length, + time1)); + + time2 = + Math.max(0, + Math.min(this.length, + time2)); + + this.cursorTime1 = time1; + this.cursorTime2 = time2; + this.cursorTrack1 = track1; + this.cursorTrack2 = track2; + + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); } Timeline.prototype.setCursor2 = function(time, trackIndex) { + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirtyPixels(time, 5); + this.markDirty(this.cursorTime2, time); + time = Math.max(0, Math.min(this.length, time)); + var lastTrack2 = this.cursorTrack2; + this.cursorTime2 = time; if (this.cursorTrack1 != 0) this.cursorTrack2 = trackIndex; -} - - -Timeline.prototype.mouseToClient = function(ev) -{ - var rect = this.canvas.getBoundingClientRect(); - return { - x: ev.clientX - rect.left - this.OFFSET_X, - xScrolled: ev.clientX - rect.left - this.OFFSET_X + this.scrollTime * this.timeToPixelsScaling, - y: ev.clientY - rect.top - }; -} - - -Timeline.prototype.handleContextMenu = function(ev) -{ - ev.preventDefault(); - return false; -} - - -Timeline.prototype.handleMouseDown = function(ev) -{ - var that = this; - - ev.preventDefault(); - - var ctrl = ev.ctrlKey; - var mousePos = this.mouseToClient(ev); - var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); - - this.mouseAction = this.INTERACT_NONE; - this.mouseDown = true; - this.mouseDownPos = mousePos; - this.mouseDownScrollTime = this.scrollTime; - this.mouseMoveDeltaTime = 0; - this.mouseMoveDeltaPitch = 0; - this.mouseMoveScrollY = 0; - this.mouseStretchTimePivot = 0; - this.mouseStretchTimeOrigin = 0; - - if (this.cursorVisible) + if (this.cursorTrack2 != lastTrack2) { this.markDirtyPixels(this.cursorTime1, 5); this.markDirtyPixels(this.cursorTime2, 5); this.markDirty(this.cursorTime1, this.cursorTime2); } - - if (ev.which !== 1) - this.mouseAction = this.INTERACT_SCROLL; - else - { - // Handle multi-selection with Ctrl. - if (!ctrl && (this.hoverElement == null || !this.hoverElement.selected)) - this.unselectAll(); - - if (this.hoverElement != null && this.hoverRegion != null) - { - this.cursorVisible = false; - - // Handle selection of element under mouse. - if (!this.hoverElement.selected) - this.select(this.hoverElement); - - // Set mouse action to a common action of - // all selected elements. - this.mouseAction = this.hoverRegion.kind; - for (var i = 0; i < this.selectedElements.length; i++) - this.mouseAction &= this.selectedElements[i].interactKind; - - // Redraw all selected elements to - // indicate multiple modification. - this.markDirtyAllSelectedElements(0); - - // Set up stretch values, if applicable. - if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0) - { - this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().end; - this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.start; - } - else if ((this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) - { - this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().start; - this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.end; - } - } - - else - { - var trackIndex = this.getTrackIndexAtY(mousePos.y); - if (trackIndex != -1) - { - this.mouseAction = this.INTERACT_CURSOR; - this.cursorVisible = true; - - this.setCursor(mouseTime, trackIndex); - this.markDirtyPixels(mouseTime, 5); - } - } - } - - var trackIndex = this.getTrackIndexAtY(mousePos.y); - this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); - - this.redraw(); -} - - -Timeline.prototype.handleMouseMove = function(ev) -{ - var that = this; - - ev.preventDefault(); - - var mousePos = this.mouseToClient(ev); - var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); - - // Handle dragging with the mouse. - if (this.mouseDown) - { - var mouseTimeDelta = (mousePos.xScrolled - this.mouseDownPos.xScrolled) / this.timeToPixelsScaling; - var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); - - // Handle scrolling. - if (this.mouseAction == this.INTERACT_SCROLL) - { - var scrollTimeDelta = (this.mouseDownPos.x - mousePos.x) / this.timeToPixelsScaling; - this.scrollTime = - Math.max(0, - Math.min(this.length - 960, - this.mouseDownScrollTime + scrollTimeDelta)); - - this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; - this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; - - this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); - this.markDirtyAll(); - } - - // Handle cursor selection. - else if (this.mouseAction == this.INTERACT_CURSOR) - { - this.markDirtyPixels(this.cursorTime2, 5); - this.markDirtyPixels(mouseTime, 5); - this.markDirty(this.cursorTime2, mouseTime); - - var trackIndex = this.getTrackIndexAtY(mousePos.y); - if (trackIndex != -1) - { - this.setCursor2(mouseTime, trackIndex); - this.markDirtyPixels(this.cursorTime1, 5); - this.markDirtyPixels(this.cursorTime2, 5); - this.markDirty(this.cursorTime1, this.cursorTime2); - } - else - this.setCursor2(mouseTime, this.cursorTrack1); - } - - else if (this.selectedElements.length != 0) - { - // Handle time displacement. - if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) - { - // Get merged time ranges of all selected elements. - var allTimeRange = this.getSelectedElementsTimeRange(); - - // Mark elements' previous positions as dirty, - // to redraw over when they move away. - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - - // Calculate displacement, - // ensuring that elements cannot fall out of bounds. - if (allTimeRange == null) - this.mouseMoveDeltaTime = mouseTimeDelta; - else - this.mouseMoveDeltaTime = - Math.max(-allTimeRange.start, - Math.min(this.length - allTimeRange.end, - mouseTimeDelta)); - - this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); - - // Mark elements' new positions as dirty, - // to redraw them at wherever they move to. - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - } - // If not displacing time, mark elements as dirty in their current positions. - else - this.markDirtyAllSelectedElements(0); - - // Handle time stretching. - if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0 || - (this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) - { - // Get merged time ranges of all selected elements. - var allTimeRange = this.getSelectedElementsTimeRange(); - - // Mark elements' previous positions as dirty, - // to redraw over when they stretch away. - var prevStretchedAllTimeRange = allTimeRange.clone(); - prevStretchedAllTimeRange.stretch( - this.mouseStretchTimePivot, - this.mouseStretchTimeOrigin, - this.mouseMoveDeltaTime); - - this.markDirtyTimeRange(prevStretchedAllTimeRange); - - // FIXME: Calculate stretch, - // ensuring that elements cannot stretch out of bounds. - this.mouseMoveDeltaTime = mouseTimeDelta; - this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); - - // Mark elements' new positions as dirty, - // to redraw them at wherever they stretch to. - var newStretchedAllTimeRange = allTimeRange.clone(); - newStretchedAllTimeRange.stretch( - this.mouseStretchTimePivot, - this.mouseStretchTimeOrigin, - this.mouseMoveDeltaTime); - - this.markDirtyTimeRange(newStretchedAllTimeRange); - } - - // Handle pitch displacement. - if ((this.mouseAction & this.INTERACT_MOVE_PITCH) != 0) - { - // Get the pitch range of all selected elements. - var pitchMin = this.selectedElements[0].interactPitch.clone(); - var pitchMax = this.selectedElements[0].interactPitch.clone(); - - for (var i = 1; i < this.selectedElements.length; i++) - { - var pitch = this.selectedElements[i].interactPitch.midiPitch; - - if (pitch < pitchMin.midiPitch) - pitchMin = new Pitch(pitch); - - if (pitch > pitchMax.midiPitch) - pitchMax = new Pitch(pitch); - } - - // Calculate displacement, - // ensuring that pitches cannot fall out of bounds. - this.mouseMoveDeltaPitch = - Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, - Math.min(this.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, - mousePitchDelta)); - } - } - } - - // If mouse is not down, just handle hovering. - else - { - if (this.hoverElement != null) - this.markDirtyElement(this.hoverElement); - - this.hoverElement = null; - this.hoverRegion = null; - - var trackIndex = this.getTrackIndexAtY(mousePos.y); - if (trackIndex != -1) - { - var track = this.tracks[trackIndex]; - - track.elements.enumerateOverlappingRange( - new TimeRange(mouseTime - 10 * this.timeToPixelsScaling, mouseTime + 10 * this.timeToPixelsScaling), - function (elem) - { - for (var e = 0; e < elem.regions.length; e++) - { - var region = elem.regions[e]; - - if (mousePos.xScrolled >= region.x && - mousePos.xScrolled <= region.x + region.width && - mousePos.y >= region.y + track.y + track.scrollY && - mousePos.y <= region.y + region.height + track.y + track.scrollY) - { - that.hoverElement = elem; - that.hoverRegion = region; - } - } - }); - } - - if (this.hoverElement != null) - this.markDirtyElement(this.hoverElement); - - var regionKind = this.INTERACT_NONE; - if (this.hoverRegion != null) - regionKind = this.hoverRegion.kind; - - if (((regionKind & this.INTERACT_MOVE_TIME) != 0) || - ((regionKind & this.INTERACT_MOVE_PITCH) != 0)) - this.canvas.style.cursor = "pointer"; - else if ((regionKind & this.INTERACT_STRETCH_TIME_L) != 0 || - (regionKind & this.INTERACT_STRETCH_TIME_R) != 0) - this.canvas.style.cursor = "ew-resize" - else - this.canvas.style.cursor = "default"; - } - - this.redraw(); -} - - -Timeline.prototype.handleMouseUp = function(ev) -{ - ev.preventDefault(); - - // Handle releasing the mouse after dragging. - if (this.mouseDown) - { - if (this.mouseAction == this.INTERACT_SCROLL) - { - if (this.mouseDownTrack != null) - this.mouseDownTrack.handleScroll(); - } - else - { - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - - for (var i = 0; i < this.selectedElements.length; i++) - this.selectedElements[i].modify(); - - for (var i = 0; i < this.tracks.length; i++) - this.tracks[i].applyModifications(); - - this.markDirtyAllSelectedElements(0); - } - } - - this.mouseDown = false; - this.mouseAction = this.INTERACT_NONE; - this.mouseDownTrack = null; - this.redraw(); } @@ -470,18 +180,21 @@ Timeline.prototype.markDirtyAll = function() } -Timeline.prototype.markDirty = function(timeStart, timeEnd) +Timeline.prototype.markDirty = function(time1, time2) { + var start = Math.min(time1, time2); + var end = Math.max(time1, time2); + if (this.redrawDirtyTimeMin == -1 || - timeStart - this.REDRAW_TIME_MARGIN < this.redrawDirtyTimeMin) + start - this.REDRAW_TIME_MARGIN < this.redrawDirtyTimeMin) { - this.redrawDirtyTimeMin = timeStart - this.REDRAW_TIME_MARGIN; + this.redrawDirtyTimeMin = start - this.REDRAW_TIME_MARGIN; } if (this.redrawDirtyTimeMax == -1 || - timeEnd + this.REDRAW_TIME_MARGIN > this.redrawDirtyTimeMax) + end + this.REDRAW_TIME_MARGIN > this.redrawDirtyTimeMax) { - this.redrawDirtyTimeMax = timeEnd + this.REDRAW_TIME_MARGIN; + this.redrawDirtyTimeMax = end + this.REDRAW_TIME_MARGIN; } } @@ -557,6 +270,26 @@ Timeline.prototype.getSelectedElementsTimeRange = function() } +Timeline.prototype.getLastElementTimeUpTo = function(trackIndex, time) +{ + var lastTime = 0; + + this.tracks[trackIndex].elements.enumerateOverlappingRange( + new TimeRange(0, time), + function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.end > lastTime && + elem.interactTimeRange.end <= time) + lastTime = elem.interactTimeRange.end; + }); + + return lastTime; +} + + Timeline.prototype.getTrackIndexAtY = function(y) { for (var i = 0; i < this.tracks.length; i++) diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js new file mode 100644 index 0000000..2836f2f --- /dev/null +++ b/src/timeline/timeline_keyboard.js @@ -0,0 +1,44 @@ +Timeline.prototype.handleKeyDown = function(ev) +{ + var that = this; + + var ctrl = ev.ctrlKey; + var shift = ev.shiftKey; + + //ev.preventDefault(); + + switch (ev.keyCode) + { + // Left arrow + case 37: + { + if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1 - this.timeSnap, + this.cursorTime1 - this.timeSnap, + this.cursorTrack1, + this.cursorTrack2); + } + + break; + } + + // Right arrow + case 39: + { + if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1 + this.timeSnap, + this.cursorTime1 + this.timeSnap, + this.cursorTrack1, + this.cursorTrack2); + } + + break; + } + } + + this.redraw(); +} \ No newline at end of file diff --git a/src/timeline/timeline_mouse.js b/src/timeline/timeline_mouse.js new file mode 100644 index 0000000..453cf9f --- /dev/null +++ b/src/timeline/timeline_mouse.js @@ -0,0 +1,363 @@ +Timeline.prototype.mouseToClient = function(ev) +{ + var rect = this.canvas.getBoundingClientRect(); + + return { + x: ev.clientX - rect.left - this.OFFSET_X, + xScrolled: ev.clientX - rect.left - this.OFFSET_X + this.scrollTime * this.timeToPixelsScaling, + y: ev.clientY - rect.top + }; +} + + +Timeline.prototype.handleContextMenu = function(ev) +{ + ev.preventDefault(); + return false; +} + + +Timeline.prototype.handleMouseDown = function(ev) +{ + var that = this; + + ev.preventDefault(); + + var ctrl = ev.ctrlKey; + var shift = ev.shiftKey; + var mousePos = this.mouseToClient(ev); + var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); + + var lastMouseDate = this.mouseDownDate; + this.mouseDownDate = new Date(); + this.mouseAction = this.INTERACT_NONE; + this.mouseDown = true; + this.mouseDownPos = mousePos; + this.mouseDownScrollTime = this.scrollTime; + this.mouseMoveDeltaTime = 0; + this.mouseMoveDeltaPitch = 0; + this.mouseMoveScrollY = 0; + this.mouseStretchTimePivot = 0; + this.mouseStretchTimeOrigin = 0; + + // Handle scrolling with the middle or right mouse buttons. + if (ev.which !== 1) + { + this.mouseAction = this.INTERACT_SCROLL; + this.canvas.style.cursor = "default"; + } + + else + { + // Handle multi-selection with Ctrl. + if (shift || (!ctrl && (this.hoverElement == null || !this.hoverElement.selected))) + this.unselectAll(); + + if (!shift && this.hoverElement != null && this.hoverRegion != null) + { + this.cursorVisible = false; + + // Handle selection of element under mouse. + if (!this.hoverElement.selected) + this.select(this.hoverElement); + + // Set mouse action to a common action of + // all selected elements. + this.mouseAction = this.hoverRegion.kind; + for (var i = 0; i < this.selectedElements.length; i++) + this.mouseAction &= this.selectedElements[i].interactKind; + + // Redraw all selected elements to + // indicate multiple modification. + this.markDirtyAllSelectedElements(0); + + // Set up stretch values, if applicable. + if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0) + { + this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().end; + this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.start; + } + else if ((this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) + { + this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().start; + this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.end; + } + } + + // Handle cursor. + else + { + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + { + this.cursorVisible = true; + + // Handle double-click. + if (new Date().getTime() - lastMouseDate < 300) + { + var snapTime = this.getLastElementTimeUpTo(trackIndex, mouseTime); + this.setCursor(snapTime, trackIndex); + } + else + { + this.mouseAction = this.INTERACT_CURSOR; + this.setCursor(mouseTime, trackIndex); + } + + this.canvas.style.cursor = "text"; + } + } + } + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); + + this.redraw(); +} + + +Timeline.prototype.handleMouseMove = function(ev) +{ + var that = this; + + ev.preventDefault(); + + var mousePos = this.mouseToClient(ev); + var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); + + // Handle dragging with the mouse. + if (this.mouseDown) + { + var mouseTimeDelta = (mousePos.xScrolled - this.mouseDownPos.xScrolled) / this.timeToPixelsScaling; + var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); + + // Handle scrolling. + if (this.mouseAction == this.INTERACT_SCROLL) + { + var scrollTimeDelta = (this.mouseDownPos.x - mousePos.x) / this.timeToPixelsScaling; + this.scrollTime = + Math.max(0, + Math.min(this.length - 960, + this.mouseDownScrollTime + scrollTimeDelta)); + + this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; + this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; + + this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); + this.markDirtyAll(); + } + + // Handle cursor selection. + else if (this.mouseAction == this.INTERACT_CURSOR) + { + var lastTrack2 = this.cursorTrack2; + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + this.setCursor2(mouseTime, trackIndex); + else + this.setCursor2(mouseTime, this.cursorTrack1); + + if (this.cursorTime2 != this.cursorTime1) + { + this.unselectAll(); + + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + var timeRange = new TimeRange(time1, time2); + + for (var i = track1; i <= track2; i++) + { + this.tracks[i].elements.enumerateOverlappingRange(timeRange, function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.includedInRange(timeRange)) + that.select(elem); + }); + } + } + } + + else if (this.selectedElements.length != 0) + { + // Handle time displacement. + if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) + { + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they move away. + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); + + // Calculate displacement, + // ensuring that elements cannot fall out of bounds. + if (allTimeRange == null) + this.mouseMoveDeltaTime = mouseTimeDelta; + else + this.mouseMoveDeltaTime = + Math.max(-allTimeRange.start, + Math.min(this.length - allTimeRange.end, + mouseTimeDelta)); + + this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they move to. + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); + } + // If not displacing time, mark elements as dirty in their current positions. + else + this.markDirtyAllSelectedElements(0); + + // Handle time stretching. + if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0 || + (this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) + { + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they stretch away. + var prevStretchedAllTimeRange = allTimeRange.clone(); + prevStretchedAllTimeRange.stretch( + this.mouseStretchTimePivot, + this.mouseStretchTimeOrigin, + this.mouseMoveDeltaTime); + + this.markDirtyTimeRange(prevStretchedAllTimeRange); + + // FIXME: Calculate stretch, + // ensuring that elements cannot stretch out of bounds. + this.mouseMoveDeltaTime = mouseTimeDelta; + this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they stretch to. + var newStretchedAllTimeRange = allTimeRange.clone(); + newStretchedAllTimeRange.stretch( + this.mouseStretchTimePivot, + this.mouseStretchTimeOrigin, + this.mouseMoveDeltaTime); + + this.markDirtyTimeRange(newStretchedAllTimeRange); + } + + // Handle pitch displacement. + if ((this.mouseAction & this.INTERACT_MOVE_PITCH) != 0) + { + // Get the pitch range of all selected elements. + var pitchMin = this.selectedElements[0].interactPitch.clone(); + var pitchMax = this.selectedElements[0].interactPitch.clone(); + + for (var i = 1; i < this.selectedElements.length; i++) + { + var pitch = this.selectedElements[i].interactPitch.midiPitch; + + if (pitch < pitchMin.midiPitch) + pitchMin = new Pitch(pitch); + + if (pitch > pitchMax.midiPitch) + pitchMax = new Pitch(pitch); + } + + // Calculate displacement, + // ensuring that pitches cannot fall out of bounds. + this.mouseMoveDeltaPitch = + Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, + Math.min(this.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, + mousePitchDelta)); + } + } + } + + // If mouse is not down, just handle hovering. + else + { + if (this.hoverElement != null) + this.markDirtyElement(this.hoverElement); + + this.hoverElement = null; + this.hoverRegion = null; + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + { + var track = this.tracks[trackIndex]; + + track.elements.enumerateOverlappingRange( + new TimeRange(mouseTime - 10 * this.timeToPixelsScaling, mouseTime + 10 * this.timeToPixelsScaling), + function (elem) + { + for (var e = 0; e < elem.regions.length; e++) + { + var region = elem.regions[e]; + + if (mousePos.xScrolled >= region.x && + mousePos.xScrolled <= region.x + region.width && + mousePos.y >= region.y + track.y + track.scrollY && + mousePos.y <= region.y + region.height + track.y + track.scrollY) + { + that.hoverElement = elem; + that.hoverRegion = region; + } + } + }); + } + + if (this.hoverElement != null) + this.markDirtyElement(this.hoverElement); + + var regionKind = this.INTERACT_NONE; + if (this.hoverRegion != null) + regionKind = this.hoverRegion.kind; + + if (((regionKind & this.INTERACT_MOVE_TIME) != 0) || + ((regionKind & this.INTERACT_MOVE_PITCH) != 0)) + this.canvas.style.cursor = "pointer"; + else if ((regionKind & this.INTERACT_STRETCH_TIME_L) != 0 || + (regionKind & this.INTERACT_STRETCH_TIME_R) != 0) + this.canvas.style.cursor = "ew-resize" + else + this.canvas.style.cursor = "text"; + } + + this.redraw(); +} + + +Timeline.prototype.handleMouseUp = function(ev) +{ + ev.preventDefault(); + + // Handle releasing the mouse after dragging. + if (this.mouseDown) + { + if (this.mouseAction == this.INTERACT_SCROLL) + { + if (this.mouseDownTrack != null) + this.mouseDownTrack.handleScroll(); + } + else + { + this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); + + for (var i = 0; i < this.selectedElements.length; i++) + this.selectedElements[i].modify(); + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].applyModifications(); + + this.markDirtyAllSelectedElements(0); + } + } + + this.mouseDown = false; + this.mouseAction = this.INTERACT_NONE; + this.mouseDownTrack = null; + this.redraw(); +} \ No newline at end of file diff --git a/src/util/timerange.js b/src/util/timerange.js index 2c78c15..84932f9 100644 --- a/src/util/timerange.js +++ b/src/util/timerange.js @@ -62,4 +62,10 @@ TimeRange.prototype.overlapsTime = function(time) TimeRange.prototype.overlapsRange = function(other) { return this.start < other.end && this.end > other.start; +} + + +TimeRange.prototype.includedInRange = function(other) +{ + return this.start < other.end && this.end >= other.start; } \ No newline at end of file From 95215d7c0e2b7d6a4afb26f91b4f096fb3480fe3 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Wed, 31 Aug 2016 20:26:15 -0300 Subject: [PATCH 40/46] add more keyboard events --- index.html | 49 ++--- src/timeline/timeline.js | 161 ++++++++++++++-- src/timeline/timeline_interaction.js | 144 ++++++++++++++ src/timeline/timeline_keyboard.js | 277 ++++++++++++++++++++++++++- src/timeline/timeline_mouse.js | 211 +++++--------------- src/timeline/track_chords.js | 61 +++--- src/timeline/track_keys.js | 14 +- src/timeline/track_length.js | 14 +- src/timeline/track_meters.js | 14 +- src/timeline/track_notes.js | 64 ++++--- src/util/drawing.js | 10 +- src/util/math.js | 6 + 12 files changed, 743 insertions(+), 282 deletions(-) create mode 100644 src/timeline/timeline_interaction.js diff --git a/index.html b/index.html index e1eed4c..992c338 100644 --- a/index.html +++ b/index.html @@ -13,29 +13,30 @@
- - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index b3736c0..c232951 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -3,6 +3,9 @@ function Timeline(canvas) var that = this; // Set up constants. + this.MOUSE = 1; + this.KEYBOARD = 2; + this.INTERACT_NONE = 0x00; this.INTERACT_SCROLL = 0x01; this.INTERACT_CURSOR = 0x02; @@ -15,7 +18,7 @@ function Timeline(canvas) this.MAX_VALID_LENGTH = 960 * 1024; this.MIN_VALID_MIDI_PITCH = 3 * 12; this.MAX_VALID_MIDI_PITCH = 8 * 12 - 1; - this.OFFSET_X = 10; + this.OFFSET_X = 30; this.REDRAW_TIME_MARGIN = 50; // Get canvas context. @@ -31,18 +34,26 @@ function Timeline(canvas) window.onmousemove = function(ev) { that.handleMouseMove(ev); }; window.onmouseup = function(ev) { that.handleMouseUp(ev); }; window.onkeydown = function(ev) { that.handleKeyDown(ev); }; - - this.mouseDownDate = new Date(); - this.mouseDown = false; - this.mouseDownPos = null; - this.mouseDownTrack = null; - this.mouseDownScrollTime = 0; - this.mouseAction = this.INTERACT_NONE; - this.mouseMoveDeltaTime = 0; - this.mouseMovePitch = 0; - this.mouseMoveScrollY = 0; - this.mouseStretchTimePivot = 0; - this.mouseStretchTimeOrigin = 0; + window.onkeyup = function(ev) { that.handleKeyUp(ev); }; + + this.mouseDownDate = new Date(); + this.mouseDown = false; + this.mouseDownPos = null; + this.mouseDownTrack = null; + this.mouseDownScrollTime = 0; + this.mouseMoveScrollY = 0; + + this.keyboardHoldSpace = false; + + this.action = this.INTERACT_NONE; + this.actionDevice = 0; + this.actionMoveDeltaTime = 0; + this.actionMovePitch = 0; + this.actionStretchTimePivot = 0; + this.actionStretchTimeOrigin = 0; + + this.createLastPitch = 12 * 5 + theory.Fs; + this.createLastDuration = this.TIME_PER_WHOLE_NOTE / 4; this.cursorVisible = false; this.cursorTime1 = 0; @@ -56,6 +67,7 @@ function Timeline(canvas) // Set up display metrics. this.scrollTime = 0; + this.durationVisible = 0; this.firstTimeVisible = 0; this.lastTimeVisible = 0; this.timeSnap = 960 / 16; @@ -80,6 +92,9 @@ function Timeline(canvas) this.tracks.push(this.trackMeters); this.tracks.push(this.trackNotes); this.tracks.push(this.trackChords); + + this.trackNotesIndex = 3; + this.trackChordsIndex = 4; } @@ -96,6 +111,58 @@ Timeline.prototype.setSong = function(song) } +Timeline.prototype.setScrollTime = function(time) +{ + var newScrollTime = + Math.max(0, + Math.min(this.length - this.TIME_PER_WHOLE_NOTE, + time)); + + this.scrollTime = newScrollTime; + + this.durationVisible = (this.canvasWidth - this.OFFSET_X) / this.timeToPixelsScaling; + this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; + this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; + this.markDirtyAll(); +} + + +Timeline.prototype.scrollTimeIntoView = function(time) +{ + if (time < this.firstTimeVisible) + this.setScrollTime(time - this.TIME_PER_WHOLE_NOTE * 4); + + else if (time > this.lastTimeVisible) + this.setScrollTime(time - this.durationVisible + this.TIME_PER_WHOLE_NOTE * 4); +} + + +Timeline.prototype.hideCursor = function() +{ + if (!this.cursorVisible) + return; + + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + this.cursorVisible = false; +} + + +Timeline.prototype.showCursor = function() +{ + if (this.cursorVisible) + return; + + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + this.cursorVisible = true; +} + + Timeline.prototype.setCursor = function(time, trackIndex) { this.markDirtyPixels(this.cursorTime1, 5); @@ -106,6 +173,11 @@ Timeline.prototype.setCursor = function(time, trackIndex) Math.max(0, Math.min(this.length, time)); + + trackIndex = + Math.max(0, + Math.min(this.tracks.length - 1, + trackIndex)); this.cursorTime1 = time; this.cursorTime2 = time; @@ -135,6 +207,16 @@ Timeline.prototype.setCursorBoth = function(time1, time2, track1, track2) Math.min(this.length, time2)); + track1 = + Math.max(0, + Math.min(this.tracks.length - 1, + track1)); + + track2 = + Math.max(0, + Math.min(this.tracks.length - 1, + track2)); + this.cursorTime1 = time1; this.cursorTime2 = time2; this.cursorTrack1 = track1; @@ -157,6 +239,11 @@ Timeline.prototype.setCursor2 = function(time, trackIndex) Math.min(this.length, time)); + trackIndex = + Math.max(0, + Math.min(this.tracks.length - 1, + trackIndex)); + var lastTrack2 = this.cursorTrack2; this.cursorTime2 = time; @@ -170,6 +257,30 @@ Timeline.prototype.setCursor2 = function(time, trackIndex) this.markDirtyPixels(this.cursorTime2, 5); this.markDirty(this.cursorTime1, this.cursorTime2); } + + if (this.cursorTime2 != this.cursorTime1) + { + this.unselectAll(); + + var that = this; + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + var timeRange = new TimeRange(time1, time2); + + for (var i = track1; i <= track2; i++) + { + this.tracks[i].elements.enumerateOverlappingRange(timeRange, function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.includedInRange(timeRange)) + that.select(elem); + }); + } + } } @@ -254,6 +365,26 @@ Timeline.prototype.getSelectedElementsTimeRange = function() { var allTimeRange = null; + for (var i = 0; i < this.selectedElements.length; i++) + { + var timeRange = this.selectedElements[i].interactTimeRange; + if (timeRange == null) + timeRange = this.selectedElements[i].timeRange; + + if (allTimeRange == null) + allTimeRange = timeRange.clone(); + else + allTimeRange.merge(timeRange); + } + + return allTimeRange; +} + + +Timeline.prototype.getSelectedElementsInteractTimeRange = function() +{ + var allTimeRange = null; + for (var i = 0; i < this.selectedElements.length; i++) { var timeRange = this.selectedElements[i].interactTimeRange; @@ -329,9 +460,7 @@ Timeline.prototype.relayout = function() for (var i = 0; i < this.tracks.length; i++) this.tracks[i].relayout(); - this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; - this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; - + this.setScrollTime(this.scrollTime); this.markDirtyAll(); } diff --git a/src/timeline/timeline_interaction.js b/src/timeline/timeline_interaction.js new file mode 100644 index 0000000..70c5f8f --- /dev/null +++ b/src/timeline/timeline_interaction.js @@ -0,0 +1,144 @@ +Timeline.prototype.interactionBeginMoveTime = function() +{ + this.actionMoveDeltaTime = 0; +} + + +Timeline.prototype.interactionUpdateMoveTime = function(deltaFromOrigin) +{ + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsInteractTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they move away. + this.markDirtyAllSelectedElements(this.actionMoveDeltaTime); + + // Calculate displacement, + // ensuring that elements cannot fall out of bounds. + if (allTimeRange == null) + this.actionMoveDeltaTime = deltaFromOrigin; + else + this.actionMoveDeltaTime = + Math.max(-allTimeRange.start, + Math.min(this.length - allTimeRange.end, + deltaFromOrigin)); + + this.actionMoveDeltaTime = snap(this.actionMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they move to. + this.markDirtyAllSelectedElements(this.actionMoveDeltaTime); +} + + +Timeline.prototype.interactionBeginMovePitch = function() +{ + this.actionMoveDeltaPitch = 0; +} + + +Timeline.prototype.interactionUpdateMovePitch = function(deltaFromOrigin) +{ + // Get the pitch range of all selected elements. + var pitchMin = null; + var pitchMax = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + if (this.selectedElements[i].interactPitch == null) + continue; + + var pitch = this.selectedElements[i].interactPitch.midiPitch; + + if (pitchMin == null || pitch < pitchMin.midiPitch) + pitchMin = new Pitch(pitch); + + if (pitchMax == null || pitch > pitchMax.midiPitch) + pitchMax = new Pitch(pitch); + } + + // Calculate displacement, + // ensuring that pitches cannot fall out of bounds. + this.actionMoveDeltaPitch = deltaFromOrigin; + + if (pitchMin != null && pitchMax != null) + { + this.actionMoveDeltaPitch = + Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, + Math.min(this.MAX_VALID_MIDI_PITCH - pitchMax.midiPitch, + this.actionMoveDeltaPitch)); + + this.createLastPitch = Math.round((pitchMin.midiPitch + pitchMax.midiPitch) / 2); + } + + this.markDirtyAllSelectedElements(0); +} + + +Timeline.prototype.interactionBeginStretchTimeL = function(origin) +{ + this.actionStretchTimePivot = this.getSelectedElementsTimeRange().end; + this.actionStretchTimeOrigin = origin; +} + + +Timeline.prototype.interactionBeginStretchTimeR = function(origin) +{ + this.actionStretchTimePivot = this.getSelectedElementsTimeRange().start; + this.actionStretchTimeOrigin = origin; +} + + +Timeline.prototype.interactionUpdateStretchTime = function(deltaFromOrigin) +{ + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsInteractTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they stretch away. + var prevStretchedAllTimeRange = allTimeRange.clone(); + prevStretchedAllTimeRange.stretch( + this.actionStretchTimePivot, + this.actionStretchTimeOrigin, + this.actionMoveDeltaTime); + + this.markDirtyTimeRange(prevStretchedAllTimeRange); + + // FIXME: Calculate stretch, + // ensuring that elements cannot stretch out of bounds. + this.actionMoveDeltaTime = deltaFromOrigin; + this.actionMoveDeltaTime = snap(this.actionMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they stretch to. + var newStretchedAllTimeRange = allTimeRange.clone(); + newStretchedAllTimeRange.stretch( + this.actionStretchTimePivot, + this.actionStretchTimeOrigin, + this.actionMoveDeltaTime); + + this.markDirtyTimeRange(newStretchedAllTimeRange); +} + + +Timeline.prototype.interactionEnd = function() +{ + if ((this.action & this.INTERACT_MOVE_TIME) != 0) + this.markDirtyAllSelectedElements(this.actionMoveDeltaTime); + + for (var i = 0; i < this.selectedElements.length; i++) + this.selectedElements[i].modify(); + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].applyModifications(); + + this.markDirtyAllSelectedElements(0); + + this.action = this.INTERACT_NONE; + this.actionDevice = 0; + + this.actionMoveDeltaTime = 0; + this.actionMoveDeltaPitch = 0; + this.actionStretchTimePivot = 0; + this.actionStretchTimeOrigin = 0; +} \ No newline at end of file diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js index 2836f2f..6ad392f 100644 --- a/src/timeline/timeline_keyboard.js +++ b/src/timeline/timeline_keyboard.js @@ -9,16 +9,78 @@ Timeline.prototype.handleKeyDown = function(ev) switch (ev.keyCode) { + // Enter + case 13: + // Esc + case 27: + { + this.interactionEnd(); + + var elementEndTime = this.getSelectedElementsTimeRange(); + if (elementEndTime != null) + this.setCursorBoth(elementEndTime.end, elementEndTime.end, this.cursorTrack1,this.cursorTrack2); + else + this.setCursorBoth(this.cursorTime1, this.cursorTime1, this.cursorTrack1, this.cursorTrack2); + + this.showCursor(); + this.scrollTimeIntoView(this.cursorTime1); + this.unselectAll(); + break; + } + + // Space + case 32: + { + this.keyboardHoldSpace = true; + break; + } + // Left arrow case 37: { - if (this.selectedElements.length == 0) + if (this.keyboardHoldSpace) + { + this.setScrollTime(this.scrollTime - this.TIME_PER_WHOLE_NOTE); + } + else if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2 - this.timeSnap, + this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime2); + } + else if (this.selectedElements.length == 0) { this.setCursorBoth( this.cursorTime1 - this.timeSnap, this.cursorTime1 - this.timeSnap, this.cursorTrack1, this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime1); + } + else + { + if (ctrl) + { + if (this._beginKeyboardAction(this.INTERACT_STRETCH_TIME_R)) + this.interactionBeginStretchTimeR(this.getSelectedElementsTimeRange().end); + + if (this.action == this.INTERACT_STRETCH_TIME_R) + this.interactionUpdateStretchTime(this.actionMoveDeltaTime - this.timeSnap); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_TIME)) + this.interactionBeginMoveTime(); + + if (this.action == this.INTERACT_MOVE_TIME) + { + this.interactionUpdateMoveTime(this.actionMoveDeltaTime - this.timeSnap); + this.scrollTimeIntoView(this.actionMoveDeltaTime + this.getSelectedElementsTimeRange().start); + } + } } break; @@ -27,18 +89,229 @@ Timeline.prototype.handleKeyDown = function(ev) // Right arrow case 39: { - if (this.selectedElements.length == 0) + if (this.keyboardHoldSpace) + { + this.setScrollTime(this.scrollTime + this.TIME_PER_WHOLE_NOTE); + } + else if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2 + this.timeSnap, + this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime2); + } + else if (this.selectedElements.length == 0) { this.setCursorBoth( this.cursorTime1 + this.timeSnap, this.cursorTime1 + this.timeSnap, this.cursorTrack1, this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime1); + } + else + { + if (ctrl) + { + if (this._beginKeyboardAction(this.INTERACT_STRETCH_TIME_R)) + this.interactionBeginStretchTimeR(this.getSelectedElementsTimeRange().end); + + if (this.action == this.INTERACT_STRETCH_TIME_R) + this.interactionUpdateStretchTime(this.actionMoveDeltaTime + this.timeSnap); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_TIME)) + this.interactionBeginMoveTime(); + + if (this.action == this.INTERACT_MOVE_TIME) + { + this.interactionUpdateMoveTime(this.actionMoveDeltaTime + this.timeSnap); + this.scrollTimeIntoView(this.actionMoveDeltaTime + this.getSelectedElementsTimeRange().end); + } + } + } + + break; + } + + // Up arrow + case 38: + { + if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2, + this.cursorTrack2 - 1); + } + else if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1, + this.cursorTime2, + this.cursorTrack1 - 1, + this.cursorTrack1 - 1); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_PITCH)) + this.interactionBeginMovePitch(); + + if (this.action == this.INTERACT_MOVE_PITCH) + this.interactionUpdateMovePitch(this.actionMoveDeltaPitch + 1); + } + + break; + } + + // Down arrow + case 40: + { + if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2, + this.cursorTrack2 + 1); + } + else if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1, + this.cursorTime2, + this.cursorTrack1 + 1, + this.cursorTrack1 + 1); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_PITCH)) + this.interactionBeginMovePitch(); + + if (this.action == this.INTERACT_MOVE_PITCH) + this.interactionUpdateMovePitch(this.actionMoveDeltaPitch - 1); } break; } + + // S, D, G, H, J, + // Z, X, C, V, B, N, M + case 90: { this._doPitchAction(theory.C); break; } + case 83: { this._doPitchAction(theory.Cs); break; } + case 88: { this._doPitchAction(theory.D); break; } + case 68: { this._doPitchAction(theory.Ds); break; } + case 67: { this._doPitchAction(theory.E); break; } + case 86: { this._doPitchAction(theory.F); break; } + case 71: { this._doPitchAction(theory.Fs); break; } + case 66: { this._doPitchAction(theory.G); break; } + case 72: { this._doPitchAction(theory.Gs); break; } + case 78: { this._doPitchAction(theory.A); break; } + case 74: { this._doPitchAction(theory.As); break; } + case 77: { this._doPitchAction(theory.B); break; } } this.redraw(); +} + + +Timeline.prototype.handleKeyUp = function(ev) +{ + switch (ev.keyCode) + { + // Space + case 32: + { + this.keyboardHoldSpace = false; + break; + } + } +} + + +Timeline.prototype._beginKeyboardAction = function(action) +{ + if (this.action != action || + this.actionDevice != this.KEYBOARD) + { + this.interactionEnd(); + this.hideCursor(); + this.actionDevice = this.KEYBOARD; + + // Set action to a common action of + // all selected elements. + this.action = action; + for (var i = 0; i < this.selectedElements.length; i++) + this.action &= this.selectedElements[i].interactKind; + + if (this.action == action) + return true; + } + + return false; +} + + +Timeline.prototype._doPitchAction = function(pitch) +{ + this.interactionEnd(); + + if (this.selectedElements.length != 0) + { + var elementEndTime = this.getSelectedElementsTimeRange(); + if (elementEndTime != null) + this.setCursorBoth(elementEndTime.end, elementEndTime.end, this.cursorTrack1,this.cursorTrack2); + else + this.setCursorBoth(this.cursorTime1, this.cursorTime1, this.cursorTrack1, this.cursorTrack2); + + this.unselectAll(); + } + + if (this.cursorTrack1 == this.cursorTrack2 && + this.cursorTime1 == this.cursorTime2) + { + var time1 = this.cursorTime1; + var time2 = time1 + this.createLastDuration; + + if (this.cursorTrack1 == this.trackNotesIndex) + { + var baseOctave = Math.floor(this.createLastPitch / 12) * 12; + var distToOctaveAbove = Math.abs((baseOctave + 12 + pitch) - this.createLastPitch); + var distToOctaveCur = Math.abs((baseOctave + pitch) - this.createLastPitch); + var distToOctaveBelow = Math.abs((baseOctave - 12 + pitch) - this.createLastPitch); + + var nearestPitch = baseOctave + pitch; + if (distToOctaveAbove < distToOctaveCur) + nearestPitch = baseOctave + 12 + pitch; + else if (distToOctaveBelow < distToOctaveCur) + nearestPitch = baseOctave - 12 + pitch; + + nearestPitch = + Math.max(this.MIN_VALID_MIDI_PITCH, + Math.min(this.MAX_VALID_MIDI_PITCH, + nearestPitch)); + + this.createLastPitch = nearestPitch; + + var noteElem = this.trackNotes.noteAdd( + new Note( + new TimeRange(time1, time2), + new Pitch(nearestPitch))); + + this.select(noteElem); + this.setCursor(time2, this.trackNotesIndex); + } + + else if (this.cursorTrack1 == this.trackChordsIndex) + { + var chordElem = this.trackChords.chordAdd( + new Chord( + new TimeRange(time1, time2), + 0, + pitch)); + + this.select(chordElem); + this.setCursor(time2, this.trackChordsIndex); + } + } } \ No newline at end of file diff --git a/src/timeline/timeline_mouse.js b/src/timeline/timeline_mouse.js index 453cf9f..52bc233 100644 --- a/src/timeline/timeline_mouse.js +++ b/src/timeline/timeline_mouse.js @@ -22,28 +22,29 @@ Timeline.prototype.handleMouseDown = function(ev) var that = this; ev.preventDefault(); + + if (this.actionDevice != 0) + this.interactionEnd(); var ctrl = ev.ctrlKey; var shift = ev.shiftKey; + var alt = ev.altKey; var mousePos = this.mouseToClient(ev); var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); - var lastMouseDate = this.mouseDownDate; - this.mouseDownDate = new Date(); - this.mouseAction = this.INTERACT_NONE; - this.mouseDown = true; - this.mouseDownPos = mousePos; - this.mouseDownScrollTime = this.scrollTime; - this.mouseMoveDeltaTime = 0; - this.mouseMoveDeltaPitch = 0; - this.mouseMoveScrollY = 0; - this.mouseStretchTimePivot = 0; - this.mouseStretchTimeOrigin = 0; + var lastMouseDate = this.mouseDownDate; + this.mouseDownDate = new Date(); + this.action = this.INTERACT_NONE; + this.actionDevice = this.MOUSE; + this.mouseDown = true; + this.mouseDownPos = mousePos; + this.mouseDownScrollTime = this.scrollTime; + this.mouseMoveScrollY = 0; // Handle scrolling with the middle or right mouse buttons. - if (ev.which !== 1) + if (ev.which !== 1 || this.keyboardHoldSpace) { - this.mouseAction = this.INTERACT_SCROLL; + this.action = this.INTERACT_SCROLL; this.canvas.style.cursor = "default"; } @@ -55,33 +56,34 @@ Timeline.prototype.handleMouseDown = function(ev) if (!shift && this.hoverElement != null && this.hoverRegion != null) { - this.cursorVisible = false; + this.hideCursor(); // Handle selection of element under mouse. if (!this.hoverElement.selected) this.select(this.hoverElement); - // Set mouse action to a common action of + // Set action to a common action of // all selected elements. - this.mouseAction = this.hoverRegion.kind; + this.action = this.hoverRegion.kind; for (var i = 0; i < this.selectedElements.length; i++) - this.mouseAction &= this.selectedElements[i].interactKind; + this.action &= this.selectedElements[i].interactKind; // Redraw all selected elements to // indicate multiple modification. this.markDirtyAllSelectedElements(0); - // Set up stretch values, if applicable. - if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0) - { - this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().end; - this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.start; - } - else if ((this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) - { - this.mouseStretchTimePivot = this.getSelectedElementsTimeRange().start; - this.mouseStretchTimeOrigin = this.hoverElement.interactTimeRange.end; - } + // Set up interaction variables. + if ((this.action & this.INTERACT_MOVE_TIME) != 0) + this.interactionBeginMoveTime(); + + if ((this.action & this.INTERACT_MOVE_PITCH) != 0) + this.interactionBeginMovePitch(); + + if ((this.action & this.INTERACT_STRETCH_TIME_L) != 0) + this.interactionBeginStretchTimeL(this.hoverElement.interactTimeRange.start); + + if ((this.action & this.INTERACT_STRETCH_TIME_R) != 0) + this.interactionBeginStretchTimeR(this.hoverElement.interactTimeRange.end); } // Handle cursor. @@ -100,7 +102,7 @@ Timeline.prototype.handleMouseDown = function(ev) } else { - this.mouseAction = this.INTERACT_CURSOR; + this.action = this.INTERACT_CURSOR; this.setCursor(mouseTime, trackIndex); } @@ -126,29 +128,24 @@ Timeline.prototype.handleMouseMove = function(ev) var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); // Handle dragging with the mouse. - if (this.mouseDown) + if (this.actionDevice == this.MOUSE && this.mouseDown) { var mouseTimeDelta = (mousePos.xScrolled - this.mouseDownPos.xScrolled) / this.timeToPixelsScaling; var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); // Handle scrolling. - if (this.mouseAction == this.INTERACT_SCROLL) + if (this.action == this.INTERACT_SCROLL) { var scrollTimeDelta = (this.mouseDownPos.x - mousePos.x) / this.timeToPixelsScaling; - this.scrollTime = - Math.max(0, - Math.min(this.length - 960, - this.mouseDownScrollTime + scrollTimeDelta)); + this.setScrollTime(this.mouseDownScrollTime + scrollTimeDelta); - this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; - this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; - this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); + this.markDirtyAll(); } // Handle cursor selection. - else if (this.mouseAction == this.INTERACT_CURSOR) + else if (this.action == this.INTERACT_CURSOR) { var lastTrack2 = this.cursorTrack2; @@ -157,126 +154,27 @@ Timeline.prototype.handleMouseMove = function(ev) this.setCursor2(mouseTime, trackIndex); else this.setCursor2(mouseTime, this.cursorTrack1); - - if (this.cursorTime2 != this.cursorTime1) - { - this.unselectAll(); - - var time1 = Math.min(this.cursorTime1, this.cursorTime2); - var time2 = Math.max(this.cursorTime1, this.cursorTime2); - var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); - var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); - var timeRange = new TimeRange(time1, time2); - - for (var i = track1; i <= track2; i++) - { - this.tracks[i].elements.enumerateOverlappingRange(timeRange, function (elem) - { - if (elem.interactTimeRange == null) - return; - - if (elem.interactTimeRange.includedInRange(timeRange)) - that.select(elem); - }); - } - } } else if (this.selectedElements.length != 0) { // Handle time displacement. - if ((this.mouseAction & this.INTERACT_MOVE_TIME) != 0) - { - // Get merged time ranges of all selected elements. - var allTimeRange = this.getSelectedElementsTimeRange(); - - // Mark elements' previous positions as dirty, - // to redraw over when they move away. - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - - // Calculate displacement, - // ensuring that elements cannot fall out of bounds. - if (allTimeRange == null) - this.mouseMoveDeltaTime = mouseTimeDelta; - else - this.mouseMoveDeltaTime = - Math.max(-allTimeRange.start, - Math.min(this.length - allTimeRange.end, - mouseTimeDelta)); - - this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); - - // Mark elements' new positions as dirty, - // to redraw them at wherever they move to. - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - } - // If not displacing time, mark elements as dirty in their current positions. - else - this.markDirtyAllSelectedElements(0); - - // Handle time stretching. - if ((this.mouseAction & this.INTERACT_STRETCH_TIME_L) != 0 || - (this.mouseAction & this.INTERACT_STRETCH_TIME_R) != 0) - { - // Get merged time ranges of all selected elements. - var allTimeRange = this.getSelectedElementsTimeRange(); - - // Mark elements' previous positions as dirty, - // to redraw over when they stretch away. - var prevStretchedAllTimeRange = allTimeRange.clone(); - prevStretchedAllTimeRange.stretch( - this.mouseStretchTimePivot, - this.mouseStretchTimeOrigin, - this.mouseMoveDeltaTime); - - this.markDirtyTimeRange(prevStretchedAllTimeRange); - - // FIXME: Calculate stretch, - // ensuring that elements cannot stretch out of bounds. - this.mouseMoveDeltaTime = mouseTimeDelta; - this.mouseMoveDeltaTime = snap(this.mouseMoveDeltaTime, this.timeSnap); - - // Mark elements' new positions as dirty, - // to redraw them at wherever they stretch to. - var newStretchedAllTimeRange = allTimeRange.clone(); - newStretchedAllTimeRange.stretch( - this.mouseStretchTimePivot, - this.mouseStretchTimeOrigin, - this.mouseMoveDeltaTime); - - this.markDirtyTimeRange(newStretchedAllTimeRange); - } + if ((this.action & this.INTERACT_MOVE_TIME) != 0) + this.interactionUpdateMoveTime(mouseTimeDelta); // Handle pitch displacement. - if ((this.mouseAction & this.INTERACT_MOVE_PITCH) != 0) - { - // Get the pitch range of all selected elements. - var pitchMin = this.selectedElements[0].interactPitch.clone(); - var pitchMax = this.selectedElements[0].interactPitch.clone(); - - for (var i = 1; i < this.selectedElements.length; i++) - { - var pitch = this.selectedElements[i].interactPitch.midiPitch; - - if (pitch < pitchMin.midiPitch) - pitchMin = new Pitch(pitch); + if ((this.action & this.INTERACT_MOVE_PITCH) != 0) + this.interactionUpdateMovePitch(mousePitchDelta); - if (pitch > pitchMax.midiPitch) - pitchMax = new Pitch(pitch); - } - - // Calculate displacement, - // ensuring that pitches cannot fall out of bounds. - this.mouseMoveDeltaPitch = - Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, - Math.min(this.MAX_VALID_MIDI_PITCH - 1 - pitchMax.midiPitch, - mousePitchDelta)); - } + // Handle time stretching. + if ((this.action & this.INTERACT_STRETCH_TIME_L) != 0 || + (this.action & this.INTERACT_STRETCH_TIME_R) != 0) + this.interactionUpdateStretchTime(mouseTimeDelta); } } // If mouse is not down, just handle hovering. - else + else if (this.actionDevice == 0) { if (this.hoverElement != null) this.markDirtyElement(this.hoverElement); @@ -335,29 +233,20 @@ Timeline.prototype.handleMouseUp = function(ev) ev.preventDefault(); // Handle releasing the mouse after dragging. - if (this.mouseDown) + if (this.actionDevice == this.MOUSE && this.mouseDown) { - if (this.mouseAction == this.INTERACT_SCROLL) + if (this.action == this.INTERACT_SCROLL) { if (this.mouseDownTrack != null) this.mouseDownTrack.handleScroll(); } else - { - this.markDirtyAllSelectedElements(this.mouseMoveDeltaTime); - - for (var i = 0; i < this.selectedElements.length; i++) - this.selectedElements[i].modify(); - - for (var i = 0; i < this.tracks.length; i++) - this.tracks[i].applyModifications(); - - this.markDirtyAllSelectedElements(0); - } + this.interactionEnd(); + + this.actionDevice = 0; } this.mouseDown = false; - this.mouseAction = this.INTERACT_NONE; this.mouseDownTrack = null; this.redraw(); } \ No newline at end of file diff --git a/src/timeline/track_chords.js b/src/timeline/track_chords.js index d3b7e21..34bff10 100644 --- a/src/timeline/track_chords.js +++ b/src/timeline/track_chords.js @@ -14,15 +14,14 @@ TrackChords.prototype.setSong = function(song) this.selectedElements = []; for (var i = 0; i < song.chords.length; i++) - { - this._clipChords(song.chords[i].timeRange); - this._chordAdd(song.chords[i]); - } + this.chordAdd(song.chords[i]); } -TrackChords.prototype._chordAdd = function(chord) +TrackChords.prototype.chordAdd = function(chord) { + this._clipChords(chord.timeRange); + var elem = new Element(); elem.track = this; elem.chord = chord.clone(); @@ -30,6 +29,8 @@ TrackChords.prototype._chordAdd = function(chord) this.elementRefresh(elem); this.elements.add(elem); this.timeline.markDirtyElement(elem); + + return elem; } @@ -53,7 +54,7 @@ TrackChords.prototype._clipChords = function(timeRange) { var clippedChord = elem.chord.clone(); clippedChord.timeRange = parts[p]; - this._chordAdd(clippedChord); + this.chordAdd(clippedChord); } } } @@ -86,8 +87,9 @@ TrackChords.prototype.elementModify = function(elem) var modifiedElem = this.getModifiedElement(elem); - elem.chord = elem.chord.clone(); - elem.chord.timeRange = + elem.chord = elem.chord.clone(); + elem.chord.rootMidiPitch = modifiedElem.rootMidiPitch; + elem.chord.timeRange = new TimeRange(modifiedElem.start, modifiedElem.start + modifiedElem.duration); this.modifiedElements.push(elem); @@ -101,7 +103,7 @@ TrackChords.prototype.elementRefresh = function(elem) elem.timeRange = elem.chord.timeRange.clone(); elem.interactTimeRange = elem.chord.timeRange.clone(); elem.interactKind = - this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH | this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; elem.regions = [ @@ -225,30 +227,35 @@ TrackChords.prototype.redraw = function(time1, time2) TrackChords.prototype.getModifiedElement = function(elem) { - var timeRange = elem.chord.timeRange.clone(); + var timeRange = elem.chord.timeRange.clone(); + var rootMidiPitch = elem.chord.rootMidiPitch; if (elem.selected) { - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) { - timeRange.start += this.timeline.mouseMoveDeltaTime; - timeRange.end += this.timeline.mouseMoveDeltaTime; + timeRange.start += this.timeline.actionMoveDeltaTime; + timeRange.end += this.timeline.actionMoveDeltaTime; } - if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || - (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + if ((this.timeline.action & this.timeline.INTERACT_MOVE_PITCH) != 0) + rootMidiPitch = mod(rootMidiPitch + this.timeline.actionMoveDeltaPitch, 12); + + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) { timeRange.stretch( - this.timeline.mouseStretchTimePivot, - this.timeline.mouseStretchTimeOrigin, - this.timeline.mouseMoveDeltaTime); + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); } } return { - start: timeRange.start, - end: timeRange.end, - duration: timeRange.duration() + start: timeRange.start, + end: timeRange.end, + duration: timeRange.duration(), + rootMidiPitch: rootMidiPitch }; } @@ -263,12 +270,14 @@ TrackChords.prototype.drawChord = function(elem) ctx.fillStyle = "#ffffff" ctx.globalAlpha = 0.75; + // Draw chord background. ctx.fillRect( 0.5 + Math.floor(modifiedElem.start * toPixels), 0, Math.floor(modifiedElem.duration * toPixels - 1), this.height); + // Draw chord parts, one for each key region it overlaps. this.timeline.trackKeys.enumerateKeysAtRange( new TimeRange(modifiedElem.start, modifiedElem.end), function (key, start, end) @@ -277,7 +286,7 @@ TrackChords.prototype.drawChord = function(elem) var x = 0.5 + Math.floor(start * toPixels); var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)) - var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, elem.chord.rootMidiPitch); + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, modifiedElem.rootMidiPitch); if (Math.floor(degree) == degree) { ctx.fillStyle = theory.degreeColor(degree); @@ -294,7 +303,7 @@ TrackChords.prototype.drawChord = function(elem) // Draw roman symbol. var mainSymbol = theory.chordSymbolInKey( - key.scaleIndex, key.rootMidiPitch, elem.chord.rootMidiPitch, + key.scaleIndex, key.rootMidiPitch, modifiedElem.rootMidiPitch, theory.chords[elem.chord.chordIndex].uppercase); ctx.fillStyle = "#000000"; @@ -339,6 +348,7 @@ TrackChords.prototype.drawChord = function(elem) maxTextWidth - mainTextWidth - supTextWidth); }); + // Draw hover white-fade overlay. if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) { ctx.fillStyle = "#ffffff" @@ -351,6 +361,7 @@ TrackChords.prototype.drawChord = function(elem) this.height); } + // Draw selected double-stripe overlay. if (elem.selected) { ctx.fillStyle = "#ffffff" @@ -358,9 +369,9 @@ TrackChords.prototype.drawChord = function(elem) ctx.fillRect( 0.5 + Math.floor(modifiedElem.start * toPixels), - 0, + 3, Math.floor(modifiedElem.duration * toPixels - 1), - this.height); + this.height - 6); } ctx.globalAlpha = 1; diff --git a/src/timeline/track_keys.js b/src/timeline/track_keys.js index 3fea0cc..13d5216 100644 --- a/src/timeline/track_keys.js +++ b/src/timeline/track_keys.js @@ -176,17 +176,17 @@ TrackKeys.prototype.getModifiedElement = function(elem) if (elem.selected) { - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) - time += this.timeline.mouseMoveDeltaTime; + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + time += this.timeline.actionMoveDeltaTime; - if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || - (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) { time = stretch( time, - this.timeline.mouseStretchTimePivot, - this.timeline.mouseStretchTimeOrigin, - this.timeline.mouseMoveDeltaTime); + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); } } diff --git a/src/timeline/track_length.js b/src/timeline/track_length.js index b0e2a77..ff8cb70 100644 --- a/src/timeline/track_length.js +++ b/src/timeline/track_length.js @@ -128,17 +128,17 @@ TrackLength.prototype.getModifiedLengthKnob = function(elem) if (elem.selected) { - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) - length += this.timeline.mouseMoveDeltaTime; + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + length += this.timeline.actionMoveDeltaTime; - if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || - (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) { length = stretch( length, - this.timeline.mouseStretchTimePivot, - this.timeline.mouseStretchTimeOrigin, - this.timeline.mouseMoveDeltaTime); + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); } } diff --git a/src/timeline/track_meters.js b/src/timeline/track_meters.js index 71335b9..1e118bc 100644 --- a/src/timeline/track_meters.js +++ b/src/timeline/track_meters.js @@ -182,17 +182,17 @@ TrackMeters.prototype.getModifiedElement = function(elem) if (elem.selected) { - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) - time += this.timeline.mouseMoveDeltaTime; + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + time += this.timeline.actionMoveDeltaTime; - if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || - (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) { time = stretch( time, - this.timeline.mouseStretchTimePivot, - this.timeline.mouseStretchTimeOrigin, - this.timeline.mouseMoveDeltaTime); + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); } } diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 0ec9531..9c517fd 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -14,15 +14,14 @@ TrackNotes.prototype.setSong = function(song) this.selectedElements = []; for (var i = 0; i < song.notes.length; i++) - { - this._clipNotes(song.notes[i].timeRange, song.notes[i].pitch); - this._noteAdd(song.notes[i]); - } + this.noteAdd(song.notes[i]); } -TrackNotes.prototype._noteAdd = function(note) +TrackNotes.prototype.noteAdd = function(note) { + this._clipNotes(note.timeRange, note.pitch); + var elem = new Element(); elem.track = this; elem.note = note.clone(); @@ -30,6 +29,8 @@ TrackNotes.prototype._noteAdd = function(note) this.elementRefresh(elem); this.elements.add(elem); this.timeline.markDirtyElement(elem); + + return elem; } @@ -56,7 +57,7 @@ TrackNotes.prototype._clipNotes = function(timeRange, pitch) { var clippedNote = elem.note.clone(); clippedNote.timeRange = parts[p]; - this._noteAdd(clippedNote); + this.noteAdd(clippedNote); } } } @@ -212,7 +213,7 @@ TrackNotes.prototype.redraw = function(time1, time2) ctx.fillStyle = "#cccccc"; ctx.fillRect( this.timeline.length * toPixels, - this.height - noteHeight * (i - minPitch), + this.height - noteHeight * (i - minPitch + 1), (time2 - this.timeline.length) * toPixels, noteHeight - 0.5); } @@ -225,14 +226,14 @@ TrackNotes.prototype.redraw = function(time1, time2) { var x = Math.floor(time * toPixels); - ctx.moveTo(x, that.height - noteHeight * (maxPitch - minPitch)); + ctx.moveTo(x, that.height - noteHeight * (maxPitch + 1 - minPitch)); ctx.lineTo(x, that.height); if (isStrong) { - ctx.moveTo(x - 1, that.height - noteHeight * (maxPitch - minPitch)); + ctx.moveTo(x - 1, that.height - noteHeight * (maxPitch + 1 - minPitch)); ctx.lineTo(x - 1, that.height); - ctx.moveTo(x + 1, that.height - noteHeight * (maxPitch - minPitch)); + ctx.moveTo(x + 1, that.height - noteHeight * (maxPitch + 1 - minPitch)); ctx.lineTo(x + 1, that.height); } }); @@ -243,17 +244,19 @@ TrackNotes.prototype.redraw = function(time1, time2) { for (var i = Math.floor(minPitch / 12) * 12 + key.rootMidiPitch; i <= maxPitch; i += 12) { + var y = that.height - noteHeight * (i - minPitch); + ctx.strokeStyle = "#000000"; ctx.beginPath(); - ctx.moveTo(start * toPixels, that.height - noteHeight * (i - minPitch)); - ctx.lineTo( end * toPixels, that.height - noteHeight * (i - minPitch)); + ctx.moveTo(start * toPixels, y); + ctx.lineTo( end * toPixels, y); if (i == 5 * 12 + key.rootMidiPitch || i == 6 * 12 + key.rootMidiPitch) { - ctx.moveTo(start * toPixels, that.height - noteHeight * (i - minPitch) - 1); - ctx.lineTo( end * toPixels, that.height - noteHeight * (i - minPitch) - 1); - ctx.moveTo(start * toPixels, that.height - noteHeight * (i - minPitch) + 1); - ctx.lineTo( end * toPixels, that.height - noteHeight * (i - minPitch) + 1); + ctx.moveTo(start * toPixels, y - 1); + ctx.lineTo( end * toPixels, y - 1); + ctx.moveTo(start * toPixels, y + 1); + ctx.lineTo( end * toPixels, y + 1); } ctx.stroke(); @@ -296,22 +299,22 @@ TrackNotes.prototype.getModifiedElement = function(elem) if (elem.selected) { - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_TIME) != 0) + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) { - timeRange.start += this.timeline.mouseMoveDeltaTime; - timeRange.end += this.timeline.mouseMoveDeltaTime; + timeRange.start += this.timeline.actionMoveDeltaTime; + timeRange.end += this.timeline.actionMoveDeltaTime; } - if ((this.timeline.mouseAction & this.timeline.INTERACT_MOVE_PITCH) != 0) - pitch += this.timeline.mouseMoveDeltaPitch; + if ((this.timeline.action & this.timeline.INTERACT_MOVE_PITCH) != 0) + pitch += this.timeline.actionMoveDeltaPitch; - if ((this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || - (this.timeline.mouseAction & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) { timeRange.stretch( - this.timeline.mouseStretchTimePivot, - this.timeline.mouseStretchTimeOrigin, - this.timeline.mouseMoveDeltaTime); + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); } } @@ -333,12 +336,12 @@ TrackNotes.prototype.getModifiedScrollY = function() var scrollY = this.scrollY; if (this.timeline.mouseDownTrack == this && - this.timeline.mouseAction == this.timeline.INTERACT_SCROLL) + this.timeline.action == this.timeline.INTERACT_SCROLL) { scrollY += this.timeline.mouseMoveScrollY; scrollY = Math.max(0, - Math.min((maxPitch - minPitch) * noteHeight - this.height, + Math.min((maxPitch + 1 - minPitch) * noteHeight - this.height, scrollY)); } @@ -357,11 +360,14 @@ TrackNotes.prototype.drawNote = function(elem) var modifiedElem = this.getModifiedElement(elem); var y = 0.5 + that.height - noteHeight * (modifiedElem.pitch - minPitch); + + // Clamp Y position for out-of-view peek. y = Math.max(-scrollY + 3, Math.min(that.height - scrollY + noteHeight - 2, y)); + // Draw note parts, one for each key region it overlaps. this.timeline.trackKeys.enumerateKeysAtRange( new TimeRange(modifiedElem.start, modifiedElem.end), function (key, start, end) @@ -384,6 +390,7 @@ TrackNotes.prototype.drawNote = function(elem) } }); + // Draw hover white-fade overlay. if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) { ctx.fillStyle = "#ffffff" @@ -396,6 +403,7 @@ TrackNotes.prototype.drawNote = function(elem) noteHeight - 1); } + // Draw selected double-stripe overlay. if (elem.selected) { ctx.fillStyle = "#ffffff" diff --git a/src/util/drawing.js b/src/util/drawing.js index a84d496..7b81a54 100644 --- a/src/util/drawing.js +++ b/src/util/drawing.js @@ -13,12 +13,12 @@ function drawStripedRect(ctx, x, y, w, h, color1, color2) ctx.lineWidth = 4; ctx.beginPath(); - var lineX = 0; - while (lineX - h * 0.6 < w) + var stripeX = -(x % 10); + while (stripeX - h * 0.6 < w) { - ctx.moveTo(x + lineX + 5, y - 5); - ctx.lineTo(x + lineX - h * 0.6 - 5, y + h + 5); - lineX += 10; + ctx.moveTo(x + stripeX + 5, y - 5); + ctx.lineTo(x + stripeX - h * 0.6 - 5, y + h + 5); + stripeX += 10; } ctx.stroke(); diff --git a/src/util/math.js b/src/util/math.js index d10fb19..f971820 100644 --- a/src/util/math.js +++ b/src/util/math.js @@ -4,6 +4,12 @@ function snap(x, step) } +function mod(x, m) +{ + return (x % m + m) % m; +} + + function stretch(x, pivot, origin, delta) { var dist = (origin - pivot); From 5424fe84859f844ad181eab7a2e1db94a9a5d8bc Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 1 Sep 2016 21:34:26 -0300 Subject: [PATCH 41/46] add pitch auto-scrolling --- main.js | 2 +- src/timeline/timeline.js | 93 +++++++++++++++++++++------- src/timeline/timeline_interaction.js | 25 ++------ src/timeline/timeline_keyboard.js | 26 +++++++- src/timeline/track_notes.js | 50 +++++++++++++++ 5 files changed, 152 insertions(+), 44 deletions(-) diff --git a/main.js b/main.js index 22fb8ad..4164019 100644 --- a/main.js +++ b/main.js @@ -24,8 +24,8 @@ function main() song.chordAdd(new Chord(new TimeRange(960 * 3, 960 * 4), 22, theory.As)); var timeline = new Timeline(canvas); - timeline.setSong(song); timeline.relayout(); + timeline.setSong(song); timeline.redraw(); mainTimeline = timeline; diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index c232951..9ff64a4 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -28,6 +28,24 @@ function Timeline(canvas) this.canvasWidth = 0; this.canvasHeight = 0; + // Set up tracks. + this.length = 0; + this.trackLength = new TrackLength(this); + this.trackKeys = new TrackKeys(this); + this.trackMeters = new TrackMeters(this); + this.trackNotes = new TrackNotes(this); + this.trackChords = new TrackChords(this); + + this.tracks = []; + this.tracks.push(this.trackLength); + this.tracks.push(this.trackKeys); + this.tracks.push(this.trackMeters); + this.tracks.push(this.trackNotes); + this.tracks.push(this.trackChords); + + this.trackNotesIndex = 3; + this.trackChordsIndex = 4; + // Set up mouse/keyboard interaction. this.canvas.oncontextmenu = function(ev) { that.handleContextMenu(ev); }; this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; @@ -55,11 +73,11 @@ function Timeline(canvas) this.createLastPitch = 12 * 5 + theory.Fs; this.createLastDuration = this.TIME_PER_WHOLE_NOTE / 4; - this.cursorVisible = false; + this.cursorVisible = true; this.cursorTime1 = 0; - this.cursorTrack1 = 0; + this.cursorTrack1 = this.trackNotesIndex; this.cursorTime2 = 0; - this.cursorTrack2 = 0; + this.cursorTrack2 = this.trackNotesIndex; this.hoverElement = null; this.hoverRegion = null; @@ -77,24 +95,6 @@ function Timeline(canvas) this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; - - // Set up tracks. - this.length = 0; - this.trackLength = new TrackLength(this); - this.trackKeys = new TrackKeys(this); - this.trackMeters = new TrackMeters(this); - this.trackNotes = new TrackNotes(this); - this.trackChords = new TrackChords(this); - - this.tracks = []; - this.tracks.push(this.trackLength); - this.tracks.push(this.trackKeys); - this.tracks.push(this.trackMeters); - this.tracks.push(this.trackNotes); - this.tracks.push(this.trackChords); - - this.trackNotesIndex = 3; - this.trackChordsIndex = 4; } @@ -123,10 +123,29 @@ Timeline.prototype.setScrollTime = function(time) this.durationVisible = (this.canvasWidth - this.OFFSET_X) / this.timeToPixelsScaling; this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; + this.markDirtyAll(); } +Timeline.prototype.setScrollPitchAtBottom = function(pitch) +{ + this.trackNotes.setScrollPitchAtBottom(pitch); +} + + +Timeline.prototype.setScrollPitchAtCenter = function(pitch) +{ + this.trackNotes.setScrollPitchAtCenter(pitch); +} + + +Timeline.prototype.getScrollPitchAtBottom = function() +{ + return this.trackNotes.getScrollPitchAtBottom(); +} + + Timeline.prototype.scrollTimeIntoView = function(time) { if (time < this.firstTimeVisible) @@ -137,6 +156,12 @@ Timeline.prototype.scrollTimeIntoView = function(time) } +Timeline.prototype.scrollPitchIntoView = function(pitch) +{ + this.trackNotes.scrollPitchIntoView(pitch); +} + + Timeline.prototype.hideCursor = function() { if (!this.cursorVisible) @@ -401,6 +426,32 @@ Timeline.prototype.getSelectedElementsInteractTimeRange = function() } +Timeline.prototype.getSelectedElementsPitchRange = function() +{ + var pitchMin = null; + var pitchMax = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + if (this.selectedElements[i].interactPitch == null) + continue; + + var pitch = this.selectedElements[i].interactPitch.midiPitch; + + if (pitchMin == null || pitch < pitchMin.midiPitch) + pitchMin = new Pitch(pitch); + + if (pitchMax == null || pitch > pitchMax.midiPitch) + pitchMax = new Pitch(pitch); + } + + return { + min: pitchMin, + max: pitchMax + }; +} + + Timeline.prototype.getLastElementTimeUpTo = function(trackIndex, time) { var lastTime = 0; diff --git a/src/timeline/timeline_interaction.js b/src/timeline/timeline_interaction.js index 70c5f8f..2060027 100644 --- a/src/timeline/timeline_interaction.js +++ b/src/timeline/timeline_interaction.js @@ -40,35 +40,20 @@ Timeline.prototype.interactionBeginMovePitch = function() Timeline.prototype.interactionUpdateMovePitch = function(deltaFromOrigin) { // Get the pitch range of all selected elements. - var pitchMin = null; - var pitchMax = null; - - for (var i = 0; i < this.selectedElements.length; i++) - { - if (this.selectedElements[i].interactPitch == null) - continue; - - var pitch = this.selectedElements[i].interactPitch.midiPitch; - - if (pitchMin == null || pitch < pitchMin.midiPitch) - pitchMin = new Pitch(pitch); - - if (pitchMax == null || pitch > pitchMax.midiPitch) - pitchMax = new Pitch(pitch); - } + var pitchRange = this.getSelectedElementsPitchRange(); // Calculate displacement, // ensuring that pitches cannot fall out of bounds. this.actionMoveDeltaPitch = deltaFromOrigin; - if (pitchMin != null && pitchMax != null) + if (pitchRange.min != null && pitchRange.max != null) { this.actionMoveDeltaPitch = - Math.max(this.MIN_VALID_MIDI_PITCH - pitchMin.midiPitch, - Math.min(this.MAX_VALID_MIDI_PITCH - pitchMax.midiPitch, + Math.max(this.MIN_VALID_MIDI_PITCH - pitchRange.min.midiPitch, + Math.min(this.MAX_VALID_MIDI_PITCH - pitchRange.max.midiPitch, this.actionMoveDeltaPitch)); - this.createLastPitch = Math.round((pitchMin.midiPitch + pitchMax.midiPitch) / 2); + this.createLastPitch = Math.round((pitchRange.min.midiPitch + pitchRange.max.midiPitch) / 2); } this.markDirtyAllSelectedElements(0); diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js index 6ad392f..b23f060 100644 --- a/src/timeline/timeline_keyboard.js +++ b/src/timeline/timeline_keyboard.js @@ -140,7 +140,12 @@ Timeline.prototype.handleKeyDown = function(ev) // Up arrow case 38: { - if (shift && this.cursorVisible) + if (this.keyboardHoldSpace) + { + this.setScrollPitchAtBottom( + this.getScrollPitchAtBottom() + 4); + } + else if (shift && this.cursorVisible) { this.setCursor2( this.cursorTime2, @@ -160,7 +165,13 @@ Timeline.prototype.handleKeyDown = function(ev) this.interactionBeginMovePitch(); if (this.action == this.INTERACT_MOVE_PITCH) + { this.interactionUpdateMovePitch(this.actionMoveDeltaPitch + 1); + + var pitchRange = this.getSelectedElementsPitchRange(); + if (pitchRange.max != null) + this.scrollPitchIntoView(this.actionMoveDeltaPitch + pitchRange.max.midiPitch); + } } break; @@ -169,7 +180,12 @@ Timeline.prototype.handleKeyDown = function(ev) // Down arrow case 40: { - if (shift && this.cursorVisible) + if (this.keyboardHoldSpace) + { + this.setScrollPitchAtBottom( + this.getScrollPitchAtBottom() - 4); + } + else if (shift && this.cursorVisible) { this.setCursor2( this.cursorTime2, @@ -189,7 +205,13 @@ Timeline.prototype.handleKeyDown = function(ev) this.interactionBeginMovePitch(); if (this.action == this.INTERACT_MOVE_PITCH) + { this.interactionUpdateMovePitch(this.actionMoveDeltaPitch - 1); + + var pitchRange = this.getSelectedElementsPitchRange(); + if (pitchRange.min != null) + this.scrollPitchIntoView(this.actionMoveDeltaPitch + pitchRange.min.midiPitch); + } } break; diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 9c517fd..de9fa21 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -15,6 +15,8 @@ TrackNotes.prototype.setSong = function(song) for (var i = 0; i < song.notes.length; i++) this.noteAdd(song.notes[i]); + + this.setScrollPitchAtCenter(12 * 5 + theory.F + 0.5); } @@ -70,6 +72,54 @@ TrackNotes.prototype.handleScroll = function() } +TrackNotes.prototype.getScrollPitchAtBottom = function() +{ + return this.timeline.MIN_VALID_MIDI_PITCH + this.scrollY / this.timeline.noteHeight; +} + + +TrackNotes.prototype.getScrollPitchAtTop = function() +{ + return this.getScrollPitchAtBottom() + this.height / this.timeline.noteHeight; +} + + +TrackNotes.prototype.setScrollPitchAtBottom = function(pitch) +{ + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; + + this.scrollY = + Math.floor( + Math.max(0, + Math.min((maxPitch + 1 - minPitch) * noteHeight - this.height, + (pitch - minPitch) * noteHeight))); + + this.timeline.markDirtyAll(); +} + + +TrackNotes.prototype.setScrollPitchAtCenter = function(pitch) +{ + this.setScrollPitchAtBottom(pitch + 0.5 - this.height / this.timeline.noteHeight / 2); +} + + +TrackNotes.prototype.scrollPitchIntoView = function(pitch) +{ + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; + + if (pitch < this.getScrollPitchAtBottom()) + this.setScrollPitchAtBottom(pitch - 4); + + else if (pitch + 1 > this.getScrollPitchAtTop()) + this.setScrollPitchAtBottom(pitch + 4 - this.height / noteHeight); +} + + TrackNotes.prototype.applyModifications = function() { for (var i = 0; i < this.modifiedElements.length; i++) From 88bdde414b1157c3e8505940d6c2fe77de5833c5 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Thu, 1 Sep 2016 22:07:35 -0300 Subject: [PATCH 42/46] add note and chord clipping out of song bounds --- src/timeline/timeline.js | 17 +++++++++++++++++ src/timeline/timeline_keyboard.js | 8 ++++++-- src/timeline/timeline_mouse.js | 5 ++--- src/timeline/track_chords.js | 10 ++++++++++ src/timeline/track_notes.js | 10 ++++++++++ src/util/timerange.js | 7 +++++++ 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 9ff64a4..6943f18 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -380,12 +380,29 @@ Timeline.prototype.unselectAll = function() Timeline.prototype.select = function(elem) { + if (elem.selected) + return; + elem.select(); this.selectedElements.push(elem); this.markDirtyElement(elem); } +Timeline.prototype.unselect = function(elem) +{ + if (!elem.selected) + return; + + elem.unselect(); + this.markDirtyElement(elem); + + var indexOf = this.selectedElements.indexOf(elem); + if (indexOf >= 0) + this.selectedElements.splice(indexOf, 1); +} + + Timeline.prototype.getSelectedElementsTimeRange = function() { var allTimeRange = null; diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js index b23f060..6c37a3f 100644 --- a/src/timeline/timeline_keyboard.js +++ b/src/timeline/timeline_keyboard.js @@ -320,7 +320,9 @@ Timeline.prototype._doPitchAction = function(pitch) new TimeRange(time1, time2), new Pitch(nearestPitch))); - this.select(noteElem); + if (noteElem != null) + this.select(noteElem); + this.setCursor(time2, this.trackNotesIndex); } @@ -332,7 +334,9 @@ Timeline.prototype._doPitchAction = function(pitch) 0, pitch)); - this.select(chordElem); + if (chordElem != null) + this.select(chordElem); + this.setCursor(time2, this.trackChordsIndex); } } diff --git a/src/timeline/timeline_mouse.js b/src/timeline/timeline_mouse.js index 52bc233..d37d8ab 100644 --- a/src/timeline/timeline_mouse.js +++ b/src/timeline/timeline_mouse.js @@ -58,9 +58,8 @@ Timeline.prototype.handleMouseDown = function(ev) { this.hideCursor(); - // Handle selection of element under mouse. - if (!this.hoverElement.selected) - this.select(this.hoverElement); + // Select element under mouse. + this.select(this.hoverElement); // Set action to a common action of // all selected elements. diff --git a/src/timeline/track_chords.js b/src/timeline/track_chords.js index 34bff10..7375749 100644 --- a/src/timeline/track_chords.js +++ b/src/timeline/track_chords.js @@ -26,6 +26,10 @@ TrackChords.prototype.chordAdd = function(chord) elem.track = this; elem.chord = chord.clone(); + elem.chord.timeRange.clip(0, this.timeline.length); + if (elem.chord.timeRange.duration() <= 0) + return null; + this.elementRefresh(elem); this.elements.add(elem); this.timeline.markDirtyElement(elem); @@ -65,12 +69,18 @@ TrackChords.prototype.applyModifications = function() for (var i = 0; i < this.modifiedElements.length; i++) { var elem = this.modifiedElements[i]; + elem.chord.timeRange.clip(0, this.timeline.length); this._clipChords(elem.chord.timeRange); } for (var i = 0; i < this.modifiedElements.length; i++) { var elem = this.modifiedElements[i]; + if (elem.chord.timeRange.duration() <= 0) + { + this.timeline.unselect(elem); + continue; + } this.elementRefresh(elem); this.elements.add(elem); diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index de9fa21..2858257 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -28,6 +28,10 @@ TrackNotes.prototype.noteAdd = function(note) elem.track = this; elem.note = note.clone(); + elem.note.timeRange.clip(0, this.timeline.length); + if (elem.note.timeRange.duration() <= 0) + return null; + this.elementRefresh(elem); this.elements.add(elem); this.timeline.markDirtyElement(elem); @@ -125,12 +129,18 @@ TrackNotes.prototype.applyModifications = function() for (var i = 0; i < this.modifiedElements.length; i++) { var elem = this.modifiedElements[i]; + elem.note.timeRange.clip(0, this.timeline.length); this._clipNotes(elem.note.timeRange, elem.note.pitch); } for (var i = 0; i < this.modifiedElements.length; i++) { var elem = this.modifiedElements[i]; + if (elem.note.timeRange.duration() <= 0) + { + this.timeline.unselect(elem); + continue; + } this.elementRefresh(elem); this.elements.add(elem); diff --git a/src/util/timerange.js b/src/util/timerange.js index 84932f9..2ece25e 100644 --- a/src/util/timerange.js +++ b/src/util/timerange.js @@ -28,6 +28,13 @@ TimeRange.prototype.stretch = function(pivot, origin, delta) } +TimeRange.prototype.clip = function(min, max) +{ + this.start = Math.max(this.start, min) + this.end = Math.min(this.end, max); +} + + TimeRange.prototype.getClippedParts = function(clipRange) { var parts = []; From 25d6e6b0d133ec35cf74e675d9d9285f79f24bba Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Tue, 6 Sep 2016 10:44:44 -0300 Subject: [PATCH 43/46] add delete/backspace interaction; fix length modification not cropping song; change keyboard scrolling key to alt --- src/timeline/element.js | 1 + src/timeline/timeline_interaction.js | 7 ++- src/timeline/timeline_keyboard.js | 85 ++++++++++++++++++++++++++-- src/timeline/timeline_mouse.js | 8 ++- src/timeline/track.js | 46 ++++++++++++++- src/timeline/track_chords.js | 6 +- src/timeline/track_keys.js | 30 ++++++---- src/timeline/track_length.js | 16 +++++- src/timeline/track_meters.js | 30 ++++++---- src/timeline/track_notes.js | 19 +++++-- src/util/timerange.js | 6 ++ 11 files changed, 214 insertions(+), 40 deletions(-) diff --git a/src/timeline/element.js b/src/timeline/element.js index b783cac..56aee58 100644 --- a/src/timeline/element.js +++ b/src/timeline/element.js @@ -28,4 +28,5 @@ function Element() }; this.modify = function() { that.track.elementModify(this); }; + this.remove = function() { that.track.elementRemove(this); }; } \ No newline at end of file diff --git a/src/timeline/timeline_interaction.js b/src/timeline/timeline_interaction.js index 2060027..e19dd80 100644 --- a/src/timeline/timeline_interaction.js +++ b/src/timeline/timeline_interaction.js @@ -53,7 +53,7 @@ Timeline.prototype.interactionUpdateMovePitch = function(deltaFromOrigin) Math.min(this.MAX_VALID_MIDI_PITCH - pitchRange.max.midiPitch, this.actionMoveDeltaPitch)); - this.createLastPitch = Math.round((pitchRange.min.midiPitch + pitchRange.max.midiPitch) / 2); + this.createLastPitch = Math.round(this.actionMoveDeltaPitch + (pitchRange.min.midiPitch + pitchRange.max.midiPitch) / 2); } this.markDirtyAllSelectedElements(0); @@ -102,6 +102,11 @@ Timeline.prototype.interactionUpdateStretchTime = function(deltaFromOrigin) this.actionStretchTimeOrigin, this.actionMoveDeltaTime); + if (this.selectedElements.length == 1) + { + this.createLastDuration = newStretchedAllTimeRange.duration(); + } + this.markDirtyTimeRange(newStretchedAllTimeRange); } diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js index 6c37a3f..7bd78a5 100644 --- a/src/timeline/timeline_keyboard.js +++ b/src/timeline/timeline_keyboard.js @@ -4,6 +4,7 @@ Timeline.prototype.handleKeyDown = function(ev) var ctrl = ev.ctrlKey; var shift = ev.shiftKey; + var alt = ev.altKey; //ev.preventDefault(); @@ -35,10 +36,79 @@ Timeline.prototype.handleKeyDown = function(ev) break; } + // Backspace + case 8: + { + var eraseToTime = 0; + + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + + this.unselectAll(); + + if (this.cursorTime1 == this.cursorTime2) + { + for (var i = track1; i <= track2; i++) + { + eraseToTime = Math.max( + this.tracks[i].getBackspaceTime(this.cursorTime1), + eraseToTime); + } + + for (var i = track1; i <= track2; i++) + { + this.tracks[i].clipRange(new TimeRange(eraseToTime, this.cursorTime1)); + this.tracks[i].sanitize(); + } + + this.showCursor(); + this.setCursorBoth(eraseToTime, eraseToTime, this.cursorTrack1, this.cursorTrack2); + this.scrollTimeIntoView(eraseToTime); + } + else + { + for (var i = track1; i <= track2; i++) + { + this.tracks[i].clipRange(new TimeRange(time1, time2)); + this.tracks[i].sanitize(); + } + + this.showCursor(); + this.setCursorBoth(time1, time1, this.cursorTrack1, this.cursorTrack2); + this.scrollTimeIntoView(time1); + } + + break; + } + + // Delete + case 46: + { + for (var i = 0; i < this.selectedElements.length; i++) + { + var elem = this.selectedElements[i]; + + if (!elem.selected) + return; + + elem.unselect(); + this.markDirtyElement(elem); + elem.remove(); + } + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].sanitize(); + + this.selectedElements = []; + break; + } + // Left arrow case 37: { - if (this.keyboardHoldSpace) + if (alt) { this.setScrollTime(this.scrollTime - this.TIME_PER_WHOLE_NOTE); } @@ -89,7 +159,7 @@ Timeline.prototype.handleKeyDown = function(ev) // Right arrow case 39: { - if (this.keyboardHoldSpace) + if (alt) { this.setScrollTime(this.scrollTime + this.TIME_PER_WHOLE_NOTE); } @@ -140,7 +210,7 @@ Timeline.prototype.handleKeyDown = function(ev) // Up arrow case 38: { - if (this.keyboardHoldSpace) + if (alt) { this.setScrollPitchAtBottom( this.getScrollPitchAtBottom() + 4); @@ -180,7 +250,7 @@ Timeline.prototype.handleKeyDown = function(ev) // Down arrow case 40: { - if (this.keyboardHoldSpace) + if (alt) { this.setScrollPitchAtBottom( this.getScrollPitchAtBottom() - 4); @@ -233,6 +303,8 @@ Timeline.prototype.handleKeyDown = function(ev) case 77: { this._doPitchAction(theory.B); break; } } + this.hoverElement = null; + this.hoverRegion = null; this.redraw(); } @@ -314,7 +386,7 @@ Timeline.prototype._doPitchAction = function(pitch) nearestPitch)); this.createLastPitch = nearestPitch; - + var noteElem = this.trackNotes.noteAdd( new Note( new TimeRange(time1, time2), @@ -323,6 +395,8 @@ Timeline.prototype._doPitchAction = function(pitch) if (noteElem != null) this.select(noteElem); + this.scrollTimeIntoView(time2); + this.scrollPitchIntoView(nearestPitch); this.setCursor(time2, this.trackNotesIndex); } @@ -337,6 +411,7 @@ Timeline.prototype._doPitchAction = function(pitch) if (chordElem != null) this.select(chordElem); + this.scrollTimeIntoView(time2); this.setCursor(time2, this.trackChordsIndex); } } diff --git a/src/timeline/timeline_mouse.js b/src/timeline/timeline_mouse.js index d37d8ab..7b8448f 100644 --- a/src/timeline/timeline_mouse.js +++ b/src/timeline/timeline_mouse.js @@ -41,8 +41,9 @@ Timeline.prototype.handleMouseDown = function(ev) this.mouseDownScrollTime = this.scrollTime; this.mouseMoveScrollY = 0; - // Handle scrolling with the middle or right mouse buttons. - if (ev.which !== 1 || this.keyboardHoldSpace) + // Handle scrolling with the middle or right mouse buttons, + // or if Alt is held down. + if (ev.which !== 1 || alt) { this.action = this.INTERACT_SCROLL; this.canvas.style.cursor = "default"; @@ -106,6 +107,7 @@ Timeline.prototype.handleMouseDown = function(ev) } this.canvas.style.cursor = "text"; + this.scrollTimeIntoView(this.cursorTime1); } } } @@ -113,6 +115,8 @@ Timeline.prototype.handleMouseDown = function(ev) var trackIndex = this.getTrackIndexAtY(mousePos.y); this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); + this.hoverElement = null; + this.hoverRegion = null; this.redraw(); } diff --git a/src/timeline/track.js b/src/timeline/track.js index 34cb503..eb9484e 100644 --- a/src/timeline/track.js +++ b/src/timeline/track.js @@ -10,7 +10,8 @@ function Track() this.modifiedElements = []; } -Track.prototype.setSong = function(song) { } +Track.prototype.setSong = function(song) { } +Track.prototype.sanitize = function() { } Track.prototype.handleScroll = function() { } @@ -19,5 +20,48 @@ Track.prototype.elementUnselect = function(elem) { } Track.prototype.elementModify = function(elem) { } Track.prototype.applyModifications = function() { } + +Track.prototype.elementRemove = function(elem) +{ + if (elem.selected) + this.timeline.unselect(elem); + + this.timeline.markDirtyElement(elem); + this.elements.remove(elem); +} + + +Track.prototype.getBackspaceTime = function(beforeTime) +{ + var time = 0; + + this.elements.enumerateAll(function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.start > beforeTime) + return; + + if (elem.interactTimeRange.end >= beforeTime) + { + time = Math.max( + elem.interactTimeRange.start, + time); + } + else + { + time = Math.max( + elem.interactTimeRange.end, + time); + } + }); + + return time; +} + + +Track.prototype.clipRange = function(timeRange) { } + Track.prototype.relayout = function() { } Track.prototype.redraw = function(time1, time2) { } \ No newline at end of file diff --git a/src/timeline/track_chords.js b/src/timeline/track_chords.js index 7375749..36a572f 100644 --- a/src/timeline/track_chords.js +++ b/src/timeline/track_chords.js @@ -20,7 +20,7 @@ TrackChords.prototype.setSong = function(song) TrackChords.prototype.chordAdd = function(chord) { - this._clipChords(chord.timeRange); + this.clipRange(chord.timeRange); var elem = new Element(); elem.track = this; @@ -38,7 +38,7 @@ TrackChords.prototype.chordAdd = function(chord) } -TrackChords.prototype._clipChords = function(timeRange) +TrackChords.prototype.clipRange = function(timeRange) { // Check for overlapping chords and clip them. var overlapping = []; @@ -70,7 +70,7 @@ TrackChords.prototype.applyModifications = function() { var elem = this.modifiedElements[i]; elem.chord.timeRange.clip(0, this.timeline.length); - this._clipChords(elem.chord.timeRange); + this.clipRange(elem.chord.timeRange); } for (var i = 0; i < this.modifiedElements.length; i++) diff --git a/src/timeline/track_keys.js b/src/timeline/track_keys.js index 13d5216..19ab66d 100644 --- a/src/timeline/track_keys.js +++ b/src/timeline/track_keys.js @@ -20,7 +20,7 @@ TrackKeys.prototype.setSong = function(song) for (var i = 0; i < song.keys.length; i++) { - this._clipKeys(song.keys[i].time); + this.clipRange(new TimeRange(song.keys[i].time, song.keys[i].time)); this._keyAdd(song.keys[i]); } @@ -61,21 +61,23 @@ TrackKeys.prototype._keyAdd = function(key) } -TrackKeys.prototype._clipKeys = function(time) +TrackKeys.prototype.clipRange = function(timeRange) { // Check for overlapping key changes and clip them. var overlapping = []; - this.elements.enumerateOverlappingTime(time, function (elem) - { - if (elem.key.time == time) - overlapping.push(elem); - }); + this.elements.enumerateOverlappingRange( + new TimeRange(timeRange.start - 1, timeRange.end + 1), + function (elem) + { + if (timeRange.overlapsTime(elem.key.time) || + (timeRange.start == timeRange.end && elem.key.time == timeRange.start)) + overlapping.push(elem); + }); for (var i = 0; i < overlapping.length; i++) { - this.elements.remove(overlapping[i]); - this.keys.remove(overlapping[i]); + this.elementRemove(overlapping[i]); } } @@ -86,7 +88,7 @@ TrackKeys.prototype.applyModifications = function() { var elem = this.modifiedElements[i]; - this._clipKeys(elem.key.time); + this.clipRange(new TimeRange(elem.key.time, elem.key.time)); this.elementRefresh(elem); this.elements.add(elem); this.keys.add(elem); @@ -99,6 +101,14 @@ TrackKeys.prototype.applyModifications = function() } +TrackKeys.prototype.elementRemove = function(elem) +{ + this.timeline.markDirtyAll(); + this.elements.remove(elem); + this.keys.remove(elem); +} + + TrackKeys.prototype.elementModify = function(elem) { this.elements.remove(elem); diff --git a/src/timeline/track_length.js b/src/timeline/track_length.js index ff8cb70..00a3236 100644 --- a/src/timeline/track_length.js +++ b/src/timeline/track_length.js @@ -31,9 +31,18 @@ TrackLength.prototype.elementModify = function(elem) var modifiedElem = this.getModifiedLengthKnob(elem); elem.length = modifiedElem.length; - this.timeline.length = modifiedElem.length; - // FIXME: Clip elements of all tracks to the new length. + if (modifiedElem.length < this.timeline.length) + { + // Clip elements that are now out of the song's bounds. + for (var i = 0; i < this.timeline.tracks.length; i++) + { + this.timeline.tracks[i].clipRange(new TimeRange(modifiedElem.length, this.timeline.length + 1)); + this.timeline.tracks[i].sanitize(); + } + } + + this.timeline.length = modifiedElem.length; this.elementRefresh(elem); this.elements.add(elem); @@ -46,11 +55,12 @@ TrackLength.prototype.elementRefresh = function(elem) { var toPixels = this.timeline.timeToPixelsScaling; + elem.length = this.timeline.length; + elem.timeRange = new TimeRange( elem.length - this.LENGTH_KNOB_WIDTH / 2 / toPixels, elem.length + this.LENGTH_KNOB_WIDTH / 2 / toPixels); - elem.length = this.timeline.length; elem.interactKind = this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; diff --git a/src/timeline/track_meters.js b/src/timeline/track_meters.js index 1e118bc..64a9efd 100644 --- a/src/timeline/track_meters.js +++ b/src/timeline/track_meters.js @@ -20,7 +20,7 @@ TrackMeters.prototype.setSong = function(song) for (var i = 0; i < song.meters.length; i++) { - this._clipMeters(song.meters[i].time); + this.clipRange(new TimeRange(song.meters[i].time, song.meters[i].time)); this._meterAdd(song.meters[i]); } @@ -67,21 +67,23 @@ TrackMeters.prototype._meterAdd = function(meter) } -TrackMeters.prototype._clipMeters = function(time) +TrackMeters.prototype.clipRange = function(timeRange) { // Check for overlapping meter changes and clip them. var overlapping = []; - this.elements.enumerateOverlappingTime(time, function (elem) - { - if (elem.meter.time == time) - overlapping.push(elem); - }); + this.elements.enumerateOverlappingRange( + new TimeRange(timeRange.start - 1, timeRange.end + 1), + function (elem) + { + if (timeRange.overlapsTime(elem.meter.time) || + (timeRange.start == timeRange.end && elem.meter.time == timeRange.start)) + overlapping.push(elem); + }); for (var i = 0; i < overlapping.length; i++) { - this.elements.remove(overlapping[i]); - this.meters.remove(overlapping[i]); + this.elementRemove(overlapping[i]); } } @@ -92,7 +94,7 @@ TrackMeters.prototype.applyModifications = function() { var elem = this.modifiedElements[i]; - this._clipMeters(elem.meter.time); + this.clipRange(new TimeRange(elem.meter.time, elem.meter.time)); this.elementRefresh(elem); this.elements.add(elem); this.meters.add(elem); @@ -105,6 +107,14 @@ TrackMeters.prototype.applyModifications = function() } +TrackMeters.prototype.elementRemove = function(elem) +{ + this.timeline.markDirtyAll(); + this.elements.remove(elem); + this.meters.remove(elem); +} + + TrackMeters.prototype.elementModify = function(elem) { this.elements.remove(elem); diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js index 2858257..9505f43 100644 --- a/src/timeline/track_notes.js +++ b/src/timeline/track_notes.js @@ -22,7 +22,7 @@ TrackNotes.prototype.setSong = function(song) TrackNotes.prototype.noteAdd = function(note) { - this._clipNotes(note.timeRange, note.pitch); + this.clipRangeWithPitch(note.timeRange, note.pitch); var elem = new Element(); elem.track = this; @@ -40,14 +40,14 @@ TrackNotes.prototype.noteAdd = function(note) } -TrackNotes.prototype._clipNotes = function(timeRange, pitch) +TrackNotes.prototype.clipRangeWithPitch = function(timeRange, pitch) { // Check for overlapping notes and clip them. var overlapping = []; this.elements.enumerateOverlappingRange(timeRange, function (elem) { - if (elem.note.pitch.midiPitch == pitch.midiPitch) + if (pitch == null || elem.note.pitch.midiPitch == pitch.midiPitch) overlapping.push(elem); }); @@ -69,6 +69,12 @@ TrackNotes.prototype._clipNotes = function(timeRange, pitch) } +TrackNotes.prototype.clipRange = function(timeRange) +{ + this.clipRangeWithPitch(timeRange, null); +} + + TrackNotes.prototype.handleScroll = function() { this.scrollY = this.getModifiedScrollY(); @@ -129,8 +135,8 @@ TrackNotes.prototype.applyModifications = function() for (var i = 0; i < this.modifiedElements.length; i++) { var elem = this.modifiedElements[i]; + this.clipRangeWithPitch(elem.note.timeRange, elem.note.pitch); elem.note.timeRange.clip(0, this.timeline.length); - this._clipNotes(elem.note.timeRange, elem.note.pitch); } for (var i = 0; i < this.modifiedElements.length; i++) @@ -311,10 +317,13 @@ TrackNotes.prototype.redraw = function(time1, time2) ctx.moveTo(start * toPixels, y); ctx.lineTo( end * toPixels, y); - if (i == 5 * 12 + key.rootMidiPitch || i == 6 * 12 + key.rootMidiPitch) + if (i == 5 * 12 + key.rootMidiPitch) { ctx.moveTo(start * toPixels, y - 1); ctx.lineTo( end * toPixels, y - 1); + } + else if (i == 6 * 12 + key.rootMidiPitch) + { ctx.moveTo(start * toPixels, y + 1); ctx.lineTo( end * toPixels, y + 1); } diff --git a/src/util/timerange.js b/src/util/timerange.js index 2ece25e..e95a718 100644 --- a/src/util/timerange.js +++ b/src/util/timerange.js @@ -61,6 +61,12 @@ TimeRange.prototype.duration = function() TimeRange.prototype.overlapsTime = function(time) +{ + return time > this.start && time < this.end; +} + + +TimeRange.prototype.includesTime = function(time) { return time >= this.start && time < this.end; } From 653b5e606d32a2b9693719f1968bee776d906fd4 Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Tue, 6 Sep 2016 10:52:23 -0300 Subject: [PATCH 44/46] add synth --- index.html | 1 + main.js | 5 +- src/synth/synth.js | 196 ++++++++++++++++++++++++++++++ src/timeline/timeline.js | 5 +- src/timeline/timeline_keyboard.js | 3 + 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/synth/synth.js diff --git a/index.html b/index.html index 992c338..a43a6f0 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,7 @@ + diff --git a/main.js b/main.js index 4164019..29f9b57 100644 --- a/main.js +++ b/main.js @@ -23,7 +23,10 @@ function main() song.chordAdd(new Chord(new TimeRange(960 * 2, 960 * 3), 16, theory.Fs)); song.chordAdd(new Chord(new TimeRange(960 * 3, 960 * 4), 22, theory.As)); - var timeline = new Timeline(canvas); + var synth = new Synth(); + setInterval(function() { synth.process(1 / 60); }, 1000 / 60); + + var timeline = new Timeline(canvas, synth); timeline.relayout(); timeline.setSong(song); timeline.redraw(); diff --git a/src/synth/synth.js b/src/synth/synth.js new file mode 100644 index 0000000..1073f22 --- /dev/null +++ b/src/synth/synth.js @@ -0,0 +1,196 @@ +function Synth() +{ + this.audioCtx = new AudioContext(); + this.globalVolume = 0.1; + this.noteOnEvents = []; + this.noteOffEvents = []; + this.voices = []; +} + + +Synth.prototype.process = function(deltaTime) +{ + for (var i = this.noteOnEvents.length - 1; i >= 0; i--) + { + var ev = this.noteOnEvents[i]; + + if (ev.time <= 0) + { + this.voiceStart(ev.instrument, ev.midiPitch, ev.volume); + this.noteOnEvents.splice(i, 1); + } + else + ev.time -= deltaTime; + } + + for (var i = this.noteOffEvents.length - 1; i >= 0; i--) + { + var ev = this.noteOffEvents[i]; + + if (ev.time <= 0) + { + for (var j = 0; j < this.voices.length; j++) + { + var voice = this.voices[j]; + if (voice.instrument == ev.instrument && + voice.midiPitch == ev.midiPitch) + { + this.voiceOff(j); + } + } + + this.noteOffEvents.splice(i, 1); + } + else + ev.time -= deltaTime; + } + + for (var i = this.voices.length - 1; i >= 0; i--) + { + var voice = this.voices[i]; + + voice.timer += deltaTime; + + var envelope = 1; + if (voice.off) + envelope = Math.max(0, 1 - voice.timer / 0.1); + else + envelope = Math.max(1, 1.5 - voice.timer / 0.05); + + if (voice.off && voice.timer > 0.1) + { + this.voiceStop(i); + this.voices.splice(i, 1); + } + else + { + for (var j = 0; j < voice.gainNodes.length; j++) + { + if (voice.gainNodes[j] != null) + voice.gainNodes[j].gain.value = voice.amplitudes[j] * voice.volume * envelope * this.globalVolume; + } + } + } +} + + +Synth.prototype.addNoteOn = function(time, instrument, midiPitch, volume) +{ + this.noteOnEvents.push({ + time: time, + instrument: instrument, + midiPitch: midiPitch, + volume: volume + }); +} + + +Synth.prototype.addNoteOff = function(time, instrument, midiPitch) +{ + this.noteOffEvents.push({ + time: time, + instrument: instrument, + midiPitch: midiPitch + }); +} + + +Synth.prototype.voiceStart = function(instrument, midiPitch, volume) +{ + for (var i = this.voices.length - 1; i >= 0; i--) + { + var otherVoice = this.voices[i]; + if (otherVoice.instrument == instrument && + otherVoice.midiPitch == midiPitch) + { + this.voiceStop(i); + this.voices.splice(i, 1); + } + } + + var voice = { + instrument: instrument, + midiPitch: midiPitch, + volume: volume, + timer: 0, + off: false, + amplitudes: [], + oscillators: [], + gainNodes: [] + }; + + var frequencyInHertz = Math.pow(2, (midiPitch - 69) / 12) * 440; + + switch (instrument) + { + case 0: + { + // Square wave harmonics. + voice.amplitudes = [1, 0, 1 / 3, 0, 1 / 5, 0, 1 / 7, 0, 1 / 9]; + break; + } + case 1: + { + // Triangle wave harmonics. + voice.amplitudes = [1, 0, 1 / 9, 0, 1 / 25, 0, 1 / 49, 0, 1 / 81]; + break; + } + } + + for (var i = 0; i < voice.amplitudes.length; i++) + { + if (voice.amplitudes[i] == 0) + { + voice.oscillators.push(null); + voice.gainNodes.push(null); + } + else + { + var oscillator = this.audioCtx.createOscillator(); + oscillator.frequency.value = frequencyInHertz * (i + 1); + oscillator.type = "sine"; + + var gainNode = this.audioCtx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(this.audioCtx.destination); + gainNode.gain.value = voice.amplitudes[i] * volume * this.globalVolume; + + oscillator.start(0); + + voice.oscillators.push(oscillator); + voice.gainNodes.push(gainNode); + } + } + + this.voices.push(voice); +} + + +Synth.prototype.voiceOff = function(index) +{ + var voice = this.voices[index]; + voice.off = true; + voice.timer = 0; +} + + +Synth.prototype.voiceStop = function(index) +{ + var voice = this.voices[index]; + for (var i = 0; i < voice.oscillators.length; i++) + { + if (voice.oscillators[i] != null) + voice.oscillators[i].stop(0); + } +} + + +Synth.prototype.stopAll = function() +{ + for (var i = this.voices.length - 1; i >= 0; i--) + this.voiceStop(i); + + this.voices = []; + this.noteOnEvents = []; + this.noteOffEvents = []; +} \ No newline at end of file diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 6943f18..9c8489f 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -1,4 +1,4 @@ -function Timeline(canvas) +function Timeline(canvas, synth) { var that = this; @@ -27,6 +27,9 @@ function Timeline(canvas) this.canvasWidth = 0; this.canvasHeight = 0; + + // Store synth context. + this.synth = synth; // Set up tracks. this.length = 0; diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js index 7bd78a5..cf68474 100644 --- a/src/timeline/timeline_keyboard.js +++ b/src/timeline/timeline_keyboard.js @@ -398,6 +398,9 @@ Timeline.prototype._doPitchAction = function(pitch) this.scrollTimeIntoView(time2); this.scrollPitchIntoView(nearestPitch); this.setCursor(time2, this.trackNotesIndex); + + this.synth.addNoteOn(0, 1, nearestPitch, 1); + this.synth.addNoteOff(0.2, 1, nearestPitch); } else if (this.cursorTrack1 == this.trackChordsIndex) From e80332ffcca07e4ff5519a43a5daae4f6f52b1de Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Wed, 7 Sep 2016 19:47:38 -0300 Subject: [PATCH 45/46] add playback --- index.html | 1 + src/synth/synth.js | 10 +++ src/timeline/element.js | 5 ++ src/timeline/timeline.js | 101 +++++++++++++++++------------- src/timeline/timeline_keyboard.js | 8 ++- src/timeline/timeline_playback.js | 77 +++++++++++++++++++++++ 6 files changed, 158 insertions(+), 44 deletions(-) create mode 100644 src/timeline/timeline_playback.js diff --git a/index.html b/index.html index a43a6f0..ade0fac 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,7 @@ + diff --git a/src/synth/synth.js b/src/synth/synth.js index 1073f22..dacb729 100644 --- a/src/synth/synth.js +++ b/src/synth/synth.js @@ -95,6 +95,16 @@ Synth.prototype.addNoteOff = function(time, instrument, midiPitch) } +Synth.prototype.sortEvents = function() +{ + this.noteOnEvents.sort(function (a, b) + { return a.time - b.time; }); + + this.noteOffEvents.sort(function (a, b) + { return a.time - b.time; }); +} + + Synth.prototype.voiceStart = function(instrument, midiPitch, volume) { for (var i = this.voices.length - 1; i >= 0; i--) diff --git a/src/timeline/element.js b/src/timeline/element.js index 56aee58..baddb7f 100644 --- a/src/timeline/element.js +++ b/src/timeline/element.js @@ -4,6 +4,11 @@ function Element() this.track = null; this.timeRange = null; + this.note = null; + this.chord = null; + this.key = null; + this.meter = null; + this.regions = []; this.interactKind = 0; this.interactTimeRange = null; diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js index 9c8489f..f78f06d 100644 --- a/src/timeline/timeline.js +++ b/src/timeline/timeline.js @@ -32,7 +32,8 @@ function Timeline(canvas, synth) this.synth = synth; // Set up tracks. - this.length = 0; + this.length = 0; + this.bpm = 120; this.trackLength = new TrackLength(this); this.trackKeys = new TrackKeys(this); this.trackMeters = new TrackMeters(this); @@ -49,6 +50,12 @@ function Timeline(canvas, synth) this.trackNotesIndex = 3; this.trackChordsIndex = 4; + // Set up playback state. + this.playing = false; + this.playingTime = 0; + this.playingTimeEnd = 0; + this.playbackInterval = null; + // Set up mouse/keyboard interaction. this.canvas.oncontextmenu = function(ev) { that.handleContextMenu(ev); }; this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; @@ -586,56 +593,66 @@ Timeline.prototype.redraw = function() } // Draw cursor. - if (this.cursorVisible) + if (this.playing) + { + this.drawCursor("#ff0000", this.playingTime, this.playingTime, 0, this.tracks.length - 1); + } + else if (this.cursorVisible) { var time1 = Math.min(this.cursorTime1, this.cursorTime2); var time2 = Math.max(this.cursorTime1, this.cursorTime2); var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); - var x1 = Math.floor(time1 * this.timeToPixelsScaling) + 0.5; - var x2 = Math.floor(time2 * this.timeToPixelsScaling) + 0.5; - var y1 = this.tracks[track1].y; - var y2 = this.tracks[track2].y + this.tracks[track2].height; - - this.ctx.strokeStyle = "#0000ff"; - this.ctx.fillStyle = "#0000ff"; - - this.ctx.beginPath(); - this.ctx.moveTo(x1, y1); - this.ctx.lineTo(x1, y2); - - this.ctx.moveTo(x2, y1); - this.ctx.lineTo(x2, y2); - this.ctx.stroke(); - - this.ctx.beginPath(); - this.ctx.moveTo(x1, y1); - this.ctx.lineTo(x1 - 4, y1 - 4); - this.ctx.lineTo(x1 + 4, y1 - 4); - this.ctx.lineTo(x1, y1); - - this.ctx.moveTo(x2, y1); - this.ctx.lineTo(x2 - 4, y1 - 4); - this.ctx.lineTo(x2 + 4, y1 - 4); - this.ctx.lineTo(x2, y1); - - this.ctx.moveTo(x1, y2); - this.ctx.lineTo(x1 - 4, y2 + 4); - this.ctx.lineTo(x1 + 4, y2 + 4); - this.ctx.lineTo(x1, y2); - - this.ctx.moveTo(x2, y2); - this.ctx.lineTo(x2 - 4, y2 + 4); - this.ctx.lineTo(x2 + 4, y2 + 4); - this.ctx.lineTo(x2, y2); - this.ctx.fill(); - - this.ctx.globalAlpha = 0.25; - this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); + this.drawCursor("#0000ff", time1, time2, track1, track2); } this.redrawDirtyTimeMin = -1; this.redrawDirtyTimeMax = -1; this.ctx.restore(); } + + +Timeline.prototype.drawCursor = function(color, time1, time2, track1, track2) +{ + var x1 = Math.floor(time1 * this.timeToPixelsScaling) + 0.5; + var x2 = Math.floor(time2 * this.timeToPixelsScaling) + 0.5; + var y1 = this.tracks[track1].y; + var y2 = this.tracks[track2].y + this.tracks[track2].height; + + this.ctx.strokeStyle = color; + this.ctx.fillStyle = color; + + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x1, y2); + + this.ctx.moveTo(x2, y1); + this.ctx.lineTo(x2, y2); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x1 - 4, y1 - 4); + this.ctx.lineTo(x1 + 4, y1 - 4); + this.ctx.lineTo(x1, y1); + + this.ctx.moveTo(x2, y1); + this.ctx.lineTo(x2 - 4, y1 - 4); + this.ctx.lineTo(x2 + 4, y1 - 4); + this.ctx.lineTo(x2, y1); + + this.ctx.moveTo(x1, y2); + this.ctx.lineTo(x1 - 4, y2 + 4); + this.ctx.lineTo(x1 + 4, y2 + 4); + this.ctx.lineTo(x1, y2); + + this.ctx.moveTo(x2, y2); + this.ctx.lineTo(x2 - 4, y2 + 4); + this.ctx.lineTo(x2 + 4, y2 + 4); + this.ctx.lineTo(x2, y2); + this.ctx.fill(); + + this.ctx.globalAlpha = 0.25; + this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); +} diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js index cf68474..6ec4bd3 100644 --- a/src/timeline/timeline_keyboard.js +++ b/src/timeline/timeline_keyboard.js @@ -33,6 +33,9 @@ Timeline.prototype.handleKeyDown = function(ev) case 32: { this.keyboardHoldSpace = true; + + this.playbackToggle(); + break; } @@ -399,8 +402,9 @@ Timeline.prototype._doPitchAction = function(pitch) this.scrollPitchIntoView(nearestPitch); this.setCursor(time2, this.trackNotesIndex); - this.synth.addNoteOn(0, 1, nearestPitch, 1); - this.synth.addNoteOff(0.2, 1, nearestPitch); + this.synth.addNoteOn(0, 0, nearestPitch, 1); + this.synth.addNoteOff(0.2, 0, nearestPitch); + this.synth.sortEvents(); } else if (this.cursorTrack1 == this.trackChordsIndex) diff --git a/src/timeline/timeline_playback.js b/src/timeline/timeline_playback.js new file mode 100644 index 0000000..001d010 --- /dev/null +++ b/src/timeline/timeline_playback.js @@ -0,0 +1,77 @@ +Timeline.prototype.playbackToggle = function() +{ + if (this.playing) + this.playbackStop(); + else + this.playbackStart(); +} + + +Timeline.prototype.playbackStart = function() +{ + this.playing = true; + + var playbackStartTime = this.cursorTime1; + var timeToSecondsScale = 60 / this.bpm / (this.TIME_PER_WHOLE_NOTE / 4); + + this.playingTime = playbackStartTime; + this.playingTimeEnd = playbackStartTime; + this.scrollTimeIntoView(playbackStartTime); + + var that = this; + this.trackNotes.elements.enumerateAll(function (elem) + { + if (elem.note == null) + return; + + if (elem.note.timeRange.end <= playbackStartTime) + return; + + that.playingTimeEnd = Math.max( + elem.note.timeRange.end, + that.playingTimeEnd); + + var startSecond = Math.max(0, (elem.note.timeRange.start - playbackStartTime) * timeToSecondsScale); + var endSecond = Math.max(0, (elem.note.timeRange.end - playbackStartTime) * timeToSecondsScale); + that.synth.addNoteOn (startSecond, 0, elem.note.pitch.midiPitch, 1); + that.synth.addNoteOff(endSecond, 0, elem.note.pitch.midiPitch); + }); + + this.synth.sortEvents(); + this.markDirtyAll(); + + this.playbackInterval = setInterval(function() { that.playbackRefresh(1 / 60); }, 1000 / 60); +} + + +Timeline.prototype.playbackStop = function() +{ + this.playing = false; + + clearInterval(this.playbackInterval); + this.playbackInterval = null; + + this.synth.stopAll(); + this.markDirtyAll(); + this.scrollTimeIntoView(this.cursorTime1); +} + + +Timeline.prototype.playbackRefresh = function(deltaSeconds) +{ + if (!this.playing) + return; + + var timeToSecondsScale = 60 / this.bpm / (this.TIME_PER_WHOLE_NOTE / 4); + + this.markDirtyPixels(this.playingTime, 5); + this.markDirtyPixels(this.playingTime + deltaSeconds / timeToSecondsScale, 5); + this.markDirty(this.playingTime, this.playingTime + deltaSeconds / timeToSecondsScale); + + this.playingTime += deltaSeconds / timeToSecondsScale; + + if (this.playingTime >= this.playingTimeEnd) + this.playbackStop(); + + this.redraw(); +} \ No newline at end of file From 3ffa3f1e58c70543d11493e42d9ca20179bfeaab Mon Sep 17 00:00:00 2001 From: Henrique Lorenzi Date: Tue, 13 Sep 2016 20:26:34 -0300 Subject: [PATCH 46/46] add chord playback --- main.js | 22 +++---- src/synth/synth.js | 98 +++++++++++++++++++++---------- src/timeline/timeline_playback.js | 86 ++++++++++++++++++++++++--- src/timeline/track_meters.js | 12 ++++ src/util/theory.js | 56 ++++++++++++++++++ 5 files changed, 224 insertions(+), 50 deletions(-) diff --git a/main.js b/main.js index 29f9b57..27c6601 100644 --- a/main.js +++ b/main.js @@ -9,19 +9,19 @@ function main() var song = new Song(); song.keyAdd(new Key(0, 0, theory.C)); - song.keyAdd(new Key(960, 0, theory.D)); - song.keyAdd(new Key(1920, 0, theory.Fs)); + //song.keyAdd(new Key(960, 0, theory.D)); + //song.keyAdd(new Key(1920, 0, theory.Fs)); song.meterAdd(new Meter(0, 4, 4)); - song.meterAdd(new Meter(720, 3, 4)); - song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(5 * 12 + 0))); - song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(5 * 12 + 1))); - song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(5 * 12 + 2))); - song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); - song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); + //song.meterAdd(new Meter(720, 3, 4)); + //song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(5 * 12 + 0))); + //song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(5 * 12 + 1))); + //song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(5 * 12 + 2))); + //song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); + //song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); song.chordAdd(new Chord(new TimeRange(960 * 0, 960 * 1), 0, theory.C)); - song.chordAdd(new Chord(new TimeRange(960 * 1, 960 * 2), 3, theory.D)); - song.chordAdd(new Chord(new TimeRange(960 * 2, 960 * 3), 16, theory.Fs)); - song.chordAdd(new Chord(new TimeRange(960 * 3, 960 * 4), 22, theory.As)); + song.chordAdd(new Chord(new TimeRange(960 * 1, 960 * 2), 0, theory.F)); + song.chordAdd(new Chord(new TimeRange(960 * 2, 960 * 3), 0, theory.G)); + song.chordAdd(new Chord(new TimeRange(960 * 3, 960 * 4), 0, theory.C)); var synth = new Synth(); setInterval(function() { synth.process(1 / 60); }, 1000 / 60); diff --git a/src/synth/synth.js b/src/synth/synth.js index dacb729..f3d21bb 100644 --- a/src/synth/synth.js +++ b/src/synth/synth.js @@ -2,6 +2,7 @@ function Synth() { this.audioCtx = new AudioContext(); this.globalVolume = 0.1; + this.time = 0; this.noteOnEvents = []; this.noteOffEvents = []; this.voices = []; @@ -10,41 +11,71 @@ function Synth() Synth.prototype.process = function(deltaTime) { - for (var i = this.noteOnEvents.length - 1; i >= 0; i--) + this.time += deltaTime; + + // Process note events. + var noteOnProcessed = 0; + var noteOffProcessed = 0; + + while (true) { - var ev = this.noteOnEvents[i]; + var processWhichKind = -1; + var nextEventTime = this.time; - if (ev.time <= 0) + // Determine which event is next up. + if (noteOnProcessed < this.noteOnEvents.length && + this.noteOnEvents[noteOnProcessed].time < nextEventTime) { - this.voiceStart(ev.instrument, ev.midiPitch, ev.volume); - this.noteOnEvents.splice(i, 1); + processWhichKind = 0; + nextEventTime = this.noteOnEvents[noteOnProcessed].time; } - else - ev.time -= deltaTime; - } - - for (var i = this.noteOffEvents.length - 1; i >= 0; i--) - { - var ev = this.noteOffEvents[i]; - if (ev.time <= 0) + if (noteOffProcessed < this.noteOffEvents.length && + this.noteOffEvents[noteOffProcessed].time < nextEventTime) + { + processWhichKind = 1; + nextEventTime = this.noteOffEvents[noteOffProcessed].time; + } + + if (processWhichKind == -1) + break; + + // Process next event. + switch (processWhichKind) { - for (var j = 0; j < this.voices.length; j++) + case 0: { - var voice = this.voices[j]; - if (voice.instrument == ev.instrument && - voice.midiPitch == ev.midiPitch) + var ev = this.noteOnEvents[noteOnProcessed]; + noteOnProcessed++; + this.voiceStart(ev.instrument, ev.midiPitch, ev.volume); + break; + } + case 1: + { + var ev = this.noteOffEvents[noteOffProcessed]; + + noteOffProcessed++; + processedEvent = true; + + for (var j = 0; j < this.voices.length; j++) { - this.voiceOff(j); + var voice = this.voices[j]; + if (voice.instrument == ev.instrument && + voice.midiPitch == ev.midiPitch) + { + this.voiceOff(j); + } } + break; } - - this.noteOffEvents.splice(i, 1); } - else - ev.time -= deltaTime; } + // Remove processed events. + this.noteOnEvents.splice(0, noteOnProcessed); + this.noteOffEvents.splice(0, noteOffProcessed); + + // Update audio output. for (var i = this.voices.length - 1; i >= 0; i--) { var voice = this.voices[i]; @@ -55,7 +86,12 @@ Synth.prototype.process = function(deltaTime) if (voice.off) envelope = Math.max(0, 1 - voice.timer / 0.1); else - envelope = Math.max(1, 1.5 - voice.timer / 0.05); + { + if (voice.timer < 0.1) + envelope = Math.max(1, 1.75 - voice.timer / 0.2); + else + envelope = Math.max(0.5, 1 - voice.timer / 0.5); + } if (voice.off && voice.timer > 0.1) { @@ -77,10 +113,10 @@ Synth.prototype.process = function(deltaTime) Synth.prototype.addNoteOn = function(time, instrument, midiPitch, volume) { this.noteOnEvents.push({ - time: time, - instrument: instrument, - midiPitch: midiPitch, - volume: volume + time: time + this.time, + instrument: instrument, + midiPitch: midiPitch, + volume: volume }); } @@ -88,9 +124,9 @@ Synth.prototype.addNoteOn = function(time, instrument, midiPitch, volume) Synth.prototype.addNoteOff = function(time, instrument, midiPitch) { this.noteOffEvents.push({ - time: time, - instrument: instrument, - midiPitch: midiPitch + time: time + this.time, + instrument: instrument, + midiPitch: midiPitch }); } @@ -203,4 +239,6 @@ Synth.prototype.stopAll = function() this.voices = []; this.noteOnEvents = []; this.noteOffEvents = []; + + this.time = 0; } \ No newline at end of file diff --git a/src/timeline/timeline_playback.js b/src/timeline/timeline_playback.js index 001d010..016690b 100644 --- a/src/timeline/timeline_playback.js +++ b/src/timeline/timeline_playback.js @@ -12,7 +12,6 @@ Timeline.prototype.playbackStart = function() this.playing = true; var playbackStartTime = this.cursorTime1; - var timeToSecondsScale = 60 / this.bpm / (this.TIME_PER_WHOLE_NOTE / 4); this.playingTime = playbackStartTime; this.playingTimeEnd = playbackStartTime; @@ -24,17 +23,68 @@ Timeline.prototype.playbackStart = function() if (elem.note == null) return; - if (elem.note.timeRange.end <= playbackStartTime) + that.playNote( + elem.note.timeRange.start, + elem.note.timeRange.end, + 0, + elem.note.pitch.midiPitch, + 1); + }); + + this.trackChords.elements.enumerateAll(function (elemChord) + { + if (elemChord.chord == null) return; - that.playingTimeEnd = Math.max( - elem.note.timeRange.end, - that.playingTimeEnd); + var bassPitch = theory.chordBassPitch(elemChord.chord.chordIndex, elemChord.chord.rootMidiPitch); + var mainPitches = theory.chordMainPitches(elemChord.chord.chordIndex, elemChord.chord.rootMidiPitch); - var startSecond = Math.max(0, (elem.note.timeRange.start - playbackStartTime) * timeToSecondsScale); - var endSecond = Math.max(0, (elem.note.timeRange.end - playbackStartTime) * timeToSecondsScale); - that.synth.addNoteOn (startSecond, 0, elem.note.pitch.midiPitch, 1); - that.synth.addNoteOff(endSecond, 0, elem.note.pitch.midiPitch); + that.trackMeters.enumerateMetersAtRange(elemChord.chord.timeRange, function (meter, start, end) + { + var bassVoicing = theory.chordBassVoicingInMeter(meter); + var mainVoicing = theory.chordMainVoicingInMeter(meter); + var beatDuration = that.TIME_PER_WHOLE_NOTE / meter.denominator; + + for (var t = meter.time; t < end; t += beatDuration * meter.numerator) + { + for (var v = 0; v < bassVoicing.length; v++) + { + var noteStart = t + bassVoicing[v][0] * beatDuration; + var noteEnd = Math.min(end, t + bassVoicing[v][1] * beatDuration); + + if (noteStart < start || + noteStart >= end) + continue; + + that.playNote( + noteStart, + noteEnd, + 1, + bassPitch, + bassVoicing[v][2]); + } + + for (var v = 0; v < mainVoicing.length; v++) + { + var noteStart = t + mainVoicing[v][0] * beatDuration; + var noteEnd = Math.min(end, t + mainVoicing[v][1] * beatDuration); + + if (noteStart < start || + noteStart >= end) + continue; + + for (var p = 0; p < mainPitches.length; p++) + { + that.playNote( + noteStart, + noteEnd, + 1, + mainPitches[p], + mainVoicing[v][2]); + } + } + } + }); }); this.synth.sortEvents(); @@ -44,6 +94,24 @@ Timeline.prototype.playbackStart = function() } +Timeline.prototype.playNote = function(start, end, instrument, midiPitch, volume) +{ + if (end < this.playingTime) + return; + + var timeToSecondsScale = 60 / this.bpm / (this.TIME_PER_WHOLE_NOTE / 4); + + this.playingTimeEnd = Math.max( + end, + this.playingTimeEnd); + + var startSecond = Math.max(0, (start - this.playingTime ) * timeToSecondsScale); + var endSecond = Math.max(0, (end - this.playingTime - 0.5) * timeToSecondsScale); + this.synth.addNoteOn (startSecond, instrument, midiPitch, volume); + this.synth.addNoteOff(endSecond, instrument, midiPitch); +} + + Timeline.prototype.playbackStop = function() { this.playing = false; diff --git a/src/timeline/track_meters.js b/src/timeline/track_meters.js index 64a9efd..f6637af 100644 --- a/src/timeline/track_meters.js +++ b/src/timeline/track_meters.js @@ -28,6 +28,18 @@ TrackMeters.prototype.setSong = function(song) } +TrackMeters.prototype.enumerateMetersAtRange = function(timeRange, callback) +{ + this.meters.enumerateAffectingRange(timeRange, function (elem, start, end) + { + if (elem == null) + return; + + callback(elem.meter, start, end); + }); +} + + TrackMeters.prototype.enumerateBeatsAtRange = function(timeRange, callback) { var that = this; diff --git a/src/util/theory.js b/src/util/theory.js index c9d08d6..9dd11fe 100644 --- a/src/util/theory.js +++ b/src/util/theory.js @@ -76,6 +76,62 @@ Theory.prototype.chordSymbolInKey = function(scaleIndex, rootMidiPitch, midiPitc } +Theory.prototype.chordBassPitch = function(chordIndex, rootMidiPitch) +{ + if (rootMidiPitch >= theory.Fs) + return 12 * 3 + rootMidiPitch; + else + return 12 * 4 + rootMidiPitch; +} + + +Theory.prototype.chordMainPitches = function(chordIndex, rootMidiPitch) +{ + var pitches = []; + var lastPitch = 12 * 5; + + for (var i = 0; i < this.chords[chordIndex].notes.length; i++) + { + var pitch = (rootMidiPitch + this.chords[chordIndex].notes[i]) % 12; + + while (pitch < lastPitch) + pitch += 12; + + pitches.push(pitch); + lastPitch = pitch; + } + + if (rootMidiPitch >= theory.Fs) + pitches = pitches.map(function (pitch) { return pitch - 12; }); + + return pitches; +} + + +Theory.prototype.chordBassVoicingInMeter = function(meter) +{ + // [ [ start beat, end beat, volume ], ... ] + switch (meter.numerator) + { + case 3: return [ [ 0, 1.5, 1 ], [ 1.5, 2, 0.7 ] ]; + case 4: return [ [ 0, 1.5, 1 ], [ 1.5, 2, 0.7 ], [ 2, 3.5, 1 ], [ 3.5, 4, 0.7 ] ]; + default: return [ ]; + } +} + + +Theory.prototype.chordMainVoicingInMeter = function(meter) +{ + // [ [ start beat, end beat, volume ], ... ] + switch (meter.numerator) + { + case 3: return [ [ 0, 1, 1 ], [ 1, 2, 0.7 ], [ 2, 3, 0.7 ] ]; + case 4: return [ [ 0, 0.9, 1 ], [ 1, 1.9, 0.5 ], [ 2, 2.9, 0.7 ], [ 3, 3.9, 0.5 ] ]; + default: return [ ]; + } +} + + Theory.prototype.degreeColor = function(degree) { switch (degree)