mirror of https://github.com/kurisufriend/0x40-web
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1404 lines
48 KiB
1404 lines
48 KiB
/* Copyright (c) William Toohey <will@mon.im>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
(function(window, document) {
|
|
"use strict";
|
|
|
|
let WAVE_PIXELS_PER_SECOND = 100;
|
|
let WAVE_HEIGHT_PIXELS = 20;
|
|
|
|
function HuesEditor(core, huesWin) {
|
|
this.buildEditSize = 80; // pixels, including header
|
|
this.buildEdit = null;
|
|
this.loopEdit = null;
|
|
this.editArea = null;
|
|
this.wrapAt = 32;
|
|
|
|
this.hilightWidth = 0;
|
|
this.hilightHeight = 0;
|
|
|
|
this.undoBuffer = [];
|
|
this.redoBuffer = [];
|
|
// Will be an array if many actions are performed in one undo
|
|
this.batchUndoArray = null;
|
|
|
|
// For rendering the waveform
|
|
this.buildWave = null;
|
|
this.loopWave = null;
|
|
this.buildWaveBuff = null;
|
|
this.loopWaveBuff = null;
|
|
this.waveContext = null;
|
|
this.waveCanvas = null;
|
|
|
|
// for storing respacks created with "new"
|
|
this.respack = null;
|
|
// when we're actually following the playing song
|
|
this.linked = false;
|
|
|
|
this.core = core;
|
|
if(core.settings.defaults.enableWindow) {
|
|
this.initUI();
|
|
core.addEventListener("beat", this.onBeat.bind(this));
|
|
core.addEventListener("newsong", this.onNewSong.bind(this));
|
|
huesWin.addTab("EDITOR", this.root);
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.initUI = function() {
|
|
this.root = document.createElement("div");
|
|
this.root.className = "editor";
|
|
let titleButtons = document.createElement("div");
|
|
titleButtons.className = "editor__title-buttons";
|
|
this.root.appendChild(titleButtons);
|
|
this.saveBtn = this.createButton("Save XML", titleButtons, true);
|
|
this.saveBtn.addEventListener("click", this.saveXML.bind(this));
|
|
this.copyBtn = this.createButton("Copy XML", titleButtons, true);
|
|
this.copyBtn.addEventListener("click", this.copyXML.bind(this));
|
|
this.undoBtn = this.createButton("Undo", titleButtons, true);
|
|
this.undoBtn.addEventListener("click", this.undo.bind(this));
|
|
this.redoBtn = this.createButton("Redo", titleButtons, true);
|
|
this.redoBtn.addEventListener("click", this.redo.bind(this));
|
|
let help = this.createButton("Help?", titleButtons);
|
|
help.style.backgroundColor = "rgba(0,160,0,0.3)";
|
|
help.addEventListener("click", () => {
|
|
window.open("https://github.com/mon/0x40-web/tree/master/docs/Editor.md", '_blank');
|
|
});
|
|
|
|
this.statusMsg = document.createElement("span");
|
|
this.statusMsg.className = "editor__status-msg";
|
|
titleButtons.appendChild(this.statusMsg);
|
|
|
|
this.topBar = document.createElement("div");
|
|
this.topBar.className = "editor__top-bar";
|
|
this.root.appendChild(this.topBar);
|
|
|
|
this.uiCreateInfo();
|
|
this.uiCreateImport();
|
|
this.root.appendChild(document.createElement("hr"));
|
|
this.uiCreateEditArea();
|
|
this.uiCreateControls();
|
|
this.uiCreateVisualiser();
|
|
|
|
document.addEventListener("keydown", e => {
|
|
e = e || window.event;
|
|
if(e.defaultPrevented) {
|
|
return true;
|
|
}
|
|
let key = e.keyCode || e.which;
|
|
|
|
if (e.ctrlKey) {
|
|
if(key == 90) { // Z
|
|
this.undo();
|
|
} else if(key == 89) { // Y
|
|
this.redo();
|
|
}
|
|
if(key == 90 || key == 89) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
window.addEventListener('resize', this.resize.bind(this));
|
|
// Fix Chrome rendering - redraw on tab load
|
|
// tabselected passes the name of the selected tab, we force noHilightCalc to false
|
|
this.core.window.addEventListener("tabselected", this.resize.bind(this, false));
|
|
this.resize();
|
|
};
|
|
|
|
HuesEditor.prototype.resize = function(noHilightCalc) {
|
|
this.root.style.height = (window.innerHeight - 200) + "px";
|
|
let boxHeight = this.editArea.offsetHeight;
|
|
let bHeadHeight = this.buildEdit._header.offsetHeight;
|
|
let lHeadHeight = this.loopEdit._header.offsetHeight;
|
|
let handleHeight = this.resizeHandle.offsetHeight;
|
|
let minHeight = bHeadHeight;
|
|
let maxHeight = boxHeight - handleHeight - lHeadHeight - bHeadHeight;
|
|
let buildHeight = Math.min(maxHeight, Math.max(minHeight, this.buildEditSize - handleHeight));
|
|
this.buildEdit.style.height = buildHeight + "px";
|
|
this.buildEdit._box.style.height = (buildHeight - bHeadHeight) + "px";
|
|
let loopHeight = maxHeight - buildHeight + lHeadHeight;
|
|
this.loopEdit.style.height = loopHeight + "px";
|
|
this.loopEdit._box.style.height = (loopHeight - lHeadHeight) + "px";
|
|
|
|
// For window resizing down situation
|
|
if(this.editArea.offsetHeight != boxHeight) {
|
|
this.resize();
|
|
}
|
|
|
|
// Resize the time lock
|
|
this.timeLock.style.height = (buildHeight + handleHeight) + "px";
|
|
|
|
// Save to fix Chrome rendering and to enable right click to seek
|
|
// We only resize on a window resize event, not when dragging the handle
|
|
if(!noHilightCalc) {
|
|
let hilight = document.createElement("div");
|
|
hilight.className = "beat-hilight";
|
|
// Because clientWidth is rounded, we need to take the average. 100 is plenty.
|
|
let grid = "";
|
|
// height goes to 99 because we always have 1 line
|
|
for(let i = 0; i < 99; i++) {
|
|
grid += "<br />";
|
|
}
|
|
// width
|
|
for(let i = 0; i < 100; i++) {
|
|
grid += " ";
|
|
}
|
|
hilight.innerHTML = grid;
|
|
this.loopEdit.appendChild(hilight);
|
|
this.hilightWidth = hilight.clientWidth / 100;
|
|
this.hilightHeight = hilight.clientHeight / 100;
|
|
this.loopEdit.removeChild(hilight);
|
|
|
|
this.waveCanvas.width = this.waveCanvas.clientWidth;
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.getOther = function(editor) {
|
|
return editor == this.loopEdit ? this.buildEdit : this.loopEdit;
|
|
};
|
|
|
|
HuesEditor.prototype.onNewSong = function(song) {
|
|
if(this.linked) {
|
|
if(song == this.song) {
|
|
// Because you can "edit current" before it loads
|
|
this.updateInfo();
|
|
this.updateWaveform();
|
|
} else {
|
|
this.linked = false;
|
|
// Clear beat hilight
|
|
this.buildEdit._hilight.innerHTML = "█";
|
|
this.loopEdit._hilight.innerHTML = "█";
|
|
this.buildEdit._hilight.className = "beat-hilight invisible";
|
|
this.loopEdit._hilight.className = "beat-hilight invisible";
|
|
// Clear the waveform
|
|
this.waveContext.clearRect(0, 0, this.waveCanvas.width, WAVE_HEIGHT_PIXELS);
|
|
}
|
|
} else if(song == this.song) { // went to another song then came back
|
|
this.linked = true;
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.onBeat = function(map, index) {
|
|
if(!this.song || this.core.currentSong != this.song) {
|
|
return;
|
|
}
|
|
let editor;
|
|
if(index < 0) {
|
|
index += this.core.currentSong.buildupRhythm.length;
|
|
editor = this.buildEdit;
|
|
this.loopEdit._hilight.className = "beat-hilight invisible";
|
|
} else {
|
|
editor = this.loopEdit;
|
|
if(this.song.buildup) {
|
|
this.buildEdit._hilight.className = "beat-hilight invisible";
|
|
}
|
|
}
|
|
editor._hilight.className = "beat-hilight";
|
|
let offsetX = index % this.wrapAt;
|
|
let offsetY = Math.floor(index / this.wrapAt);
|
|
// Not computing width/height here due to Chrome bug
|
|
editor._hilight.style.left = Math.floor(offsetX * this.hilightWidth) + "px";
|
|
editor._hilight.style.top = Math.floor(offsetY * this.hilightHeight) + "px";
|
|
};
|
|
|
|
HuesEditor.prototype.reflow = function(editor, map) {
|
|
if(!map) { // NOTHING TO SEE HERE
|
|
editor._beatmap.textContent = "";
|
|
editor._hilight.textContent = "[none]";
|
|
editor._hilight.className = "beat-hilight";
|
|
editor._hilight.style.top = "0";
|
|
editor._hilight.style.left = "0";
|
|
editor._beatCount.textContent = "0 beats";
|
|
return;
|
|
} else {
|
|
editor._hilight.innerHTML = "█";
|
|
}
|
|
editor._beatCount.textContent = map.length + " beats";
|
|
// http://stackoverflow.com/a/27012001
|
|
// if it's too long to wrap, scroll in the x direction
|
|
let regex = new RegExp("(.{" + this.wrapAt + "})", "g");
|
|
editor._beatmap.innerHTML = map.replace(regex, "$1<br />");
|
|
};
|
|
|
|
HuesEditor.prototype.updateInfo = function() {
|
|
// Avoid a bunch of nested elses
|
|
this.seekStart.classList.add("hues-button--disabled");
|
|
this.seekLoop.classList.add("hues-button--disabled");
|
|
this.saveBtn.classList.add("hues-button--disabled");
|
|
this.copyBtn.classList.add("hues-button--disabled");
|
|
this.buildEdit._removeBtn.classList.add("hues-button--disabled");
|
|
this.loopEdit._removeBtn.classList.add("hues-button--disabled");
|
|
|
|
if(!this.song) {
|
|
return;
|
|
}
|
|
|
|
this.saveBtn.classList.remove("hues-button--disabled");
|
|
this.copyBtn.classList.remove("hues-button--disabled");
|
|
|
|
if(this.song.independentBuild) {
|
|
this.timeLock._locker.innerHTML = "";
|
|
this.timeLock.classList.add("unlocked");
|
|
} else {
|
|
this.timeLock._locker.innerHTML = "";
|
|
this.timeLock.classList.remove("unlocked");
|
|
}
|
|
if(this.song.sound) {
|
|
this.seekLoop.classList.remove("hues-button--disabled");
|
|
this.loopEdit._removeBtn.classList.remove("hues-button--disabled");
|
|
}
|
|
if(this.song.buildup) {
|
|
this.seekStart.classList.remove("hues-button--disabled");
|
|
this.buildEdit._removeBtn.classList.remove("hues-button--disabled");
|
|
}
|
|
|
|
if(!this.linked) {
|
|
return;
|
|
}
|
|
|
|
let loopLen = this.core.soundManager.loopLength;
|
|
let buildLen = this.core.soundManager.buildLength;
|
|
let beatLen = (loopLen / this.song.rhythm.length) * 1000;
|
|
|
|
this.loopLen.textContent = loopLen.toFixed(2);
|
|
this.buildLen.textContent = buildLen.toFixed(2);
|
|
this.beatLen.textContent = beatLen.toFixed(2);
|
|
};
|
|
|
|
HuesEditor.prototype.loadAudio = function(editor) {
|
|
if(editor._fileInput.files.length < 1) {
|
|
return;
|
|
}
|
|
// If first load, this makes fresh, gets the core synced up
|
|
this.newSong(this.song);
|
|
|
|
// Have we just added a build to a song with a rhythm, or vice versa?
|
|
// If so, link their lengths
|
|
let newlyLinked = !this.song[editor._sound] && !!this.song[this.getOther(editor)._sound];
|
|
|
|
// Disable load button TODO
|
|
let file = editor._fileInput.files[0];
|
|
|
|
// load audio
|
|
this.blobToArrayBuffer(file)
|
|
.then(buffer => {
|
|
// Is this buffer even decodable?
|
|
let testSong = {test: buffer};
|
|
return this.core.soundManager.loadBuffer(testSong, "test")
|
|
// keep the buffer moving through the chain
|
|
// remember it's been passed through to a worker, so we update the reference
|
|
.then(() => {
|
|
return testSong.test;
|
|
});
|
|
}).then(buffer => {
|
|
this.song[editor._sound] = buffer;
|
|
// Save filename for XML export
|
|
let noExt = file.name.replace(/\.[^/.]+$/, "");
|
|
if(editor._sound == "sound") {
|
|
this.song.name = noExt;
|
|
} else {
|
|
this.song.buildupName = noExt;
|
|
}
|
|
// make empty map if needed
|
|
if(!this.getText(editor)) {
|
|
this.setText(editor, "x...o...x...o...");
|
|
}
|
|
// Do we have a loop to play?
|
|
if(this.song.sound) {
|
|
// Force refresh
|
|
return this.core.soundManager.playSong(this.song, true, true);
|
|
}
|
|
}).then(() => {
|
|
if(newlyLinked) {
|
|
this.setIndependentBuild(false);
|
|
}
|
|
this.updateInfo();
|
|
this.core.updateBeatLength();
|
|
// We may have to go backwards in time
|
|
this.core.recalcBeatIndex();
|
|
this.updateWaveform();
|
|
}).catch(error => {
|
|
console.log(error);
|
|
this.alert("Couldn't load song! Is it a LAME encoded MP3?");
|
|
});
|
|
};
|
|
|
|
HuesEditor.prototype.removeAudio = function(editor) {
|
|
if(!this.song) {
|
|
return;
|
|
}
|
|
|
|
this.song[editor._sound] = null;
|
|
this.song[editor._rhythm] = "";
|
|
this.setIndependentBuild(true);
|
|
this.reflow(editor, "");
|
|
// Is the loop playable?
|
|
if(this.song.sound && this.linked) {
|
|
this.core.soundManager.playSong(this.song, true, true)
|
|
.then(() => {
|
|
this.updateWaveform();
|
|
});
|
|
} else {
|
|
this.core.soundManager.stop();
|
|
this.updateWaveform();
|
|
}
|
|
this.updateInfo();
|
|
this.updateHalveDoubleButtons(editor);
|
|
};
|
|
|
|
HuesEditor.prototype.blobToArrayBuffer = function(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
let fr = new FileReader();
|
|
fr.onload = () => {
|
|
resolve(fr.result);
|
|
};
|
|
fr.onerror = () => {
|
|
reject(new Error("File read failed!"));
|
|
};
|
|
fr.readAsArrayBuffer(blob);
|
|
});
|
|
};
|
|
|
|
HuesEditor.prototype.newSong = function(song) {
|
|
if(!song) {
|
|
song = {"name":"Name",
|
|
"title":"Title",
|
|
"rhythm":"",
|
|
"source":"",
|
|
"sound":null,
|
|
"enabled":true,
|
|
"filename":null,
|
|
"charsPerBeat": null,
|
|
// Because new songs are empty
|
|
"independentBuild": true};
|
|
if(!this.respack) {
|
|
this.respack = new Respack();
|
|
this.respack.name = "Editor Respack";
|
|
this.respack.author = "You!";
|
|
this.respack.description = "An internal resourcepack for editing new songs";
|
|
this.core.resourceManager.addPack(this.respack);
|
|
}
|
|
this.respack.songs.push(song);
|
|
this.core.resourceManager.rebuildArrays();
|
|
this.core.resourceManager.rebuildEnabled();
|
|
this.core.setSongOject(song);
|
|
}
|
|
// Clear instructions
|
|
this.buildEdit._hilight.className = "beat-hilight invisible";
|
|
this.loopEdit._hilight.className = "beat-hilight invisible";
|
|
|
|
// Clear helpful glows
|
|
this.newSongBtn.classList.remove("hues-button--glow");
|
|
this.fromSongBtn.classList.remove("hues-button--glow");
|
|
|
|
// Enable title edits
|
|
this.title.disabled = false;
|
|
this.source.disabled = false;
|
|
|
|
this.clearUndoRedo();
|
|
|
|
this.song = song;
|
|
this.reflow(this.buildEdit, song.buildupRhythm);
|
|
this.reflow(this.loopEdit, song.rhythm);
|
|
this.title.value = song.title;
|
|
this.source.value = song.source;
|
|
|
|
// Force independent build if only 1 source is present
|
|
this.updateIndependentBuild();
|
|
|
|
// Unlock beatmap lengths
|
|
this.setLocked(this.buildEdit, 0);
|
|
this.setLocked(this.loopEdit, 0);
|
|
|
|
this.linked = true;
|
|
this.updateInfo();
|
|
this.updateWaveform();
|
|
};
|
|
|
|
HuesEditor.prototype.updateIndependentBuild = function() {
|
|
// Force independent build if only 1 source is present
|
|
|
|
// Effectively buildup XOR loop - does only 1 exist?
|
|
let hasBuild = !!this.song.buildup;
|
|
let hasLoop = !!this.song.sound;
|
|
if(hasBuild != hasLoop) {
|
|
this.setIndependentBuild(true);
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.setIndependentBuild = function(indep) {
|
|
this.song.independentBuild = indep;
|
|
if(!indep) {
|
|
// If both are locked, we lock the result, otherwise unlock both
|
|
let lock = this.loopEdit._locked && this.buildEdit._locked;
|
|
// Then unlock both so text adjustment can occur
|
|
this.loopEdit._locked = 0;
|
|
this.buildEdit._locked = 0;
|
|
// Correct the lengths
|
|
this.setText(this.loopEdit, this.getText(this.loopEdit));
|
|
// Restore locked state
|
|
if(lock) {
|
|
this.loopEdit._locked = this.song.rhythm.length;
|
|
this.buildEdit._locked = this.song.buildupRhythm.length;
|
|
}
|
|
}
|
|
this.updateInfo();
|
|
};
|
|
|
|
HuesEditor.prototype.batchUndo = function() {
|
|
if(!this.batchUndoArray)
|
|
this.batchUndoArray = [];
|
|
};
|
|
|
|
HuesEditor.prototype.commitUndo = function() {
|
|
if(this.batchUndoArray) {
|
|
this.undoBuffer.push(this.batchUndoArray);
|
|
this.trimUndo();
|
|
this.batchUndoArray = null;
|
|
this.updateUndoUI();
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.pushUndo = function(name, editor, oldText, newText) {
|
|
if(oldText == newText) {
|
|
return;
|
|
}
|
|
this.redoBuffer = [];
|
|
|
|
let undoObj = {songVar: name,
|
|
editor: editor,
|
|
text: oldText,
|
|
indep: this.song.independentBuild};
|
|
if(this.batchUndoArray) {
|
|
this.batchUndoArray.push(undoObj);
|
|
} else {
|
|
// 1 element array so undoRedo is neater
|
|
this.undoBuffer.push([undoObj]);
|
|
this.trimUndo();
|
|
}
|
|
this.updateUndoUI();
|
|
};
|
|
|
|
HuesEditor.prototype.trimUndo = function() {
|
|
while(this.undoBuffer.length > 50) {
|
|
this.undoBuffer.shift();
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.undo = function() {
|
|
this.undoRedo(this.undoBuffer, this.redoBuffer);
|
|
};
|
|
|
|
HuesEditor.prototype.redo = function() {
|
|
this.undoRedo(this.redoBuffer, this.undoBuffer);
|
|
};
|
|
|
|
HuesEditor.prototype.undoRedo = function(from, to) {
|
|
if(from.length === 0 || !this.song) {
|
|
return;
|
|
}
|
|
// Remove old data
|
|
let fromArray = from.pop();
|
|
let toArray = [];
|
|
for(let i = 0; i < fromArray.length; i++) {
|
|
let fromData = fromArray[i];
|
|
// Make restore from current
|
|
toArray.push({songVar: fromData.songVar,
|
|
editor: fromData.editor,
|
|
text: this.song[fromData.songVar],
|
|
indep: this.song.independentBuild});
|
|
// Restore to editor
|
|
this.song[fromData.songVar] = fromData.text;
|
|
this.song.independentBuild = fromData.indep;
|
|
// Don't have weird behaviour there
|
|
if(fromData.editor._locked) {
|
|
fromData.editor._locked = fromData.text.length;
|
|
}
|
|
this.reflow(fromData.editor, this.song[fromData.songVar]);
|
|
this.updateHalveDoubleButtons(fromData.editor);
|
|
}
|
|
to.push(toArray);
|
|
this.updateUndoUI();
|
|
this.updateInfo();
|
|
this.core.updateBeatLength();
|
|
this.core.recalcBeatIndex();
|
|
};
|
|
|
|
HuesEditor.prototype.clearUndoRedo = function() {
|
|
this.undoBuffer = [];
|
|
this.redoBuffer = [];
|
|
this.updateUndoUI();
|
|
};
|
|
|
|
HuesEditor.prototype.updateUndoUI = function() {
|
|
this.undoBtn.className = "hues-button hues-button--disabled";
|
|
this.redoBtn.className = "hues-button hues-button--disabled";
|
|
|
|
if(this.undoBuffer.length > 0) {
|
|
this.undoBtn.classList.remove("hues-button--disabled");
|
|
}
|
|
if(this.redoBuffer.length > 0) {
|
|
this.redoBtn.classList.remove("hues-button--disabled");
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.halveBeats = function(editor) {
|
|
let commit = false;
|
|
if(!this.song.independentBuild) {
|
|
commit = true;
|
|
this.batchUndo();
|
|
// halve them both
|
|
let other = this.getOther(editor);
|
|
this.song.independentBuild = true;
|
|
this.halveBeats(other);
|
|
}
|
|
this.setText(editor, this.song[editor._rhythm].replace(/(.)./g, "$1"));
|
|
if(commit) {
|
|
this.commitUndo();
|
|
// We set it so any rounding is padded
|
|
this.setIndependentBuild(false);
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.doubleBeats = function(editor) {
|
|
let commit = false;
|
|
if(!this.song.independentBuild) {
|
|
commit = true;
|
|
this.batchUndo();
|
|
// Double them both
|
|
let other = this.getOther(editor);
|
|
this.song.independentBuild = true;
|
|
this.doubleBeats(other);
|
|
}
|
|
this.setText(editor, this.song[editor._rhythm].replace(/(.)/g, "$1."));
|
|
if(commit) {
|
|
this.commitUndo();
|
|
// We set it so any rounding is padded
|
|
this.setIndependentBuild(false);
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.updateHalveDoubleButtons = function(editor) {
|
|
editor._halveBtn.className = "hues-button hues-button--disabled";
|
|
editor._doubleBtn.className = "hues-button hues-button--disabled";
|
|
|
|
if(!editor._locked) {
|
|
let txtLen = this.getText(editor).length;
|
|
if(!this.song.independentBuild) {
|
|
let other = this.getOther(editor);
|
|
txtLen = Math.min(txtLen, this.getText(other).length);
|
|
}
|
|
if(txtLen > 0) {
|
|
editor._doubleBtn.className = "hues-button";
|
|
}
|
|
if(txtLen > 1) {
|
|
editor._halveBtn.className = "hues-button";
|
|
}
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.createTextInput = function(label, subtitle, parent) {
|
|
let div = document.createElement("div");
|
|
div.className = "editor__label";
|
|
let caption = document.createElement("label");
|
|
caption.innerHTML = label;
|
|
div.appendChild(caption);
|
|
let container = document.createElement("span");
|
|
container.className = "editor__textinput-container";
|
|
let input = document.createElement("input");
|
|
input.className = "editor__textinput";
|
|
input.type = "text";
|
|
input.value = subtitle;
|
|
container.appendChild(input);
|
|
div.appendChild(container);
|
|
|
|
parent.appendChild(div);
|
|
|
|
return input;
|
|
};
|
|
|
|
HuesEditor.prototype.createButton = function(label, parent, disabled, extraClass) {
|
|
let button = document.createElement("span");
|
|
button.className = "hues-button";
|
|
if(disabled) {
|
|
button.classList.add("hues-button--disabled");
|
|
}
|
|
if(extraClass) {
|
|
button.className += " " + extraClass;
|
|
}
|
|
// Automagically make disabled buttons ignore clicks
|
|
button.addEventListener("click", event => {
|
|
if(button.classList.contains("hues-button--disabled")) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
});
|
|
button.innerHTML = label.toUpperCase();
|
|
parent.appendChild(button);
|
|
return button;
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateInfo = function() {
|
|
let info = document.createElement("div");
|
|
this.topBar.appendChild(info);
|
|
info.className = "editor__info";
|
|
|
|
let songUpdate = function(name) {
|
|
if(!this.song ) {
|
|
return;
|
|
}
|
|
this.song[name] = this[name].value;
|
|
if(this.song != this.core.currentSong) {
|
|
return;
|
|
}
|
|
this.core.callEventListeners("newsong", this.song);
|
|
};
|
|
|
|
this.title = this.createTextInput("Title:", "Song name", info);
|
|
this.title.oninput = songUpdate.bind(this, "title");
|
|
this.title.disabled = true;
|
|
this.source = this.createTextInput("Link: ", "Source link", info);
|
|
this.source.oninput = songUpdate.bind(this, "source");
|
|
this.source.disabled = true;
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateImport = function() {
|
|
let imports = document.createElement("div");
|
|
this.topBar.appendChild(imports);
|
|
imports.className = "editor__imports";
|
|
|
|
let songEdits = document.createElement("div");
|
|
imports.appendChild(songEdits);
|
|
let newSongBtn = this.createButton("New song", songEdits, false, "hues-button--glow");
|
|
newSongBtn.addEventListener("click", () => {
|
|
this.newSong();
|
|
});
|
|
this.newSongBtn = newSongBtn;
|
|
let fromSong = this.createButton("Edit current song", songEdits, false, "hues-button--glow");
|
|
fromSong.addEventListener("click", () => {
|
|
if(this.core.currentSong) {
|
|
this.newSong(this.core.currentSong);
|
|
}
|
|
});
|
|
this.fromSongBtn = fromSong;
|
|
|
|
let songInfos = document.createElement("div");
|
|
songInfos.className = "settings-individual editor__song-stats";
|
|
imports.appendChild(songInfos);
|
|
|
|
this.loopLen = this.uiCreateSongStat("Loop length (s):", "0.00", songInfos);
|
|
this.buildLen = this.uiCreateSongStat("Build length (s):", "0.00", songInfos);
|
|
this.beatLen = this.uiCreateSongStat("Beat length (ms):", "0.00", songInfos);
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateSongStat = function(name, value, parent) {
|
|
let container = document.createElement("div");
|
|
parent.appendChild(container);
|
|
let label = document.createElement("span");
|
|
label.textContent = name;
|
|
container.appendChild(label);
|
|
let valueSpan = document.createElement("span");
|
|
valueSpan.textContent = value;
|
|
valueSpan.className = "editor__song-stats__value";
|
|
container.appendChild(valueSpan);
|
|
return valueSpan;
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateEditArea = function() {
|
|
let editArea = document.createElement("div");
|
|
this.editArea = editArea;
|
|
editArea.className = "edit-area";
|
|
this.root.appendChild(editArea);
|
|
|
|
// Lock build/loop lengths
|
|
this.timeLock = document.createElement("div");
|
|
editArea.appendChild(this.timeLock);
|
|
this.timeLock.className = "hues-icon edit-area__timelock edit-area__timelock--unlocked";
|
|
// CHAIN-BROKEN, use  for CHAIN
|
|
let locker = this.createButton("", this.timeLock);
|
|
locker.addEventListener("click", () => {
|
|
// Only allow if both song bits exist
|
|
if(!this.song || !this.song.buildup || !this.song.sound) {
|
|
return;
|
|
}
|
|
this.setIndependentBuild(!this.song.independentBuild);
|
|
});
|
|
this.timeLock._locker = locker;
|
|
|
|
this.buildEdit = this.uiCreateSingleEditor("Buildup", "buildup", "buildupRhythm", editArea);
|
|
this.seekStart = this.buildEdit._seek;
|
|
// FIRST |<<
|
|
this.seekStart.innerHTML = "";
|
|
this.seekStart.addEventListener("click", () => {
|
|
this.core.soundManager.seek(-this.core.soundManager.buildLength);
|
|
});
|
|
|
|
// drag handle
|
|
let handleContainer = document.createElement("div");
|
|
handleContainer.className = "resize-handle";
|
|
editArea.appendChild(handleContainer);
|
|
let handle = document.createElement("div");
|
|
handle.className = 'hues-icon resize-handle__handle';
|
|
handle.innerHTML = ""; // DRAG HANDLE
|
|
handleContainer.appendChild(handle);
|
|
this.resizeHandle = handleContainer;
|
|
|
|
handleContainer.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
let editTop = this.editArea.getBoundingClientRect().top;
|
|
let handleSize = this.resizeHandle.clientHeight;
|
|
|
|
let resizer = (e) => {
|
|
this.buildEditSize = Math.floor(e.clientY - editTop + handleSize/2);
|
|
this.resize(true);
|
|
};
|
|
|
|
let mouseup = function(e) {
|
|
document.removeEventListener("mousemove", resizer);
|
|
document.removeEventListener("mouseup", mouseup);
|
|
};
|
|
|
|
document.addEventListener("mousemove", resizer);
|
|
document.addEventListener("mouseup", mouseup);
|
|
});
|
|
|
|
this.loopEdit = this.uiCreateSingleEditor("Rhythm ", "sound", "rhythm", editArea);
|
|
this.seekLoop = this.loopEdit._seek;
|
|
// FIRST |<<
|
|
this.seekLoop.innerHTML = "";
|
|
this.seekLoop.addEventListener("click", () => {
|
|
this.core.soundManager.seek(0);
|
|
});
|
|
|
|
this.buildEdit._hilight.textContent = "[none]";
|
|
this.loopEdit._hilight.innerHTML =
|
|
'<br />' +
|
|
'Click [LOAD RHYTHM] to load a loop! LAME encoded MP3s work best.<br />' +
|
|
'(LAME is important for seamless MP3 loops)<br />' +
|
|
'<br />' +
|
|
'[DOUBLE] doubles the selected map length by padding it with "."s.<br />' +
|
|
'[HALVE] shortens the map length by removing every other character.<br />' +
|
|
'<br />' +
|
|
'You can also add a buildup with [LOAD BUILDUP], or remove it<br />' +
|
|
'with [REMOVE].<br />' +
|
|
'<br />' +
|
|
'[NEW SONG] adds a completely empty song for you to edit, and<br />' +
|
|
'[EDIT CURRENT SONG] takes the current playing song to the editor.<br />' +
|
|
'<br />' +
|
|
'[COPY/SAVE XML] allow for storing the rhythms and easy <br />' +
|
|
'inclusion into a Resource Pack!';
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateSingleEditor = function(title, soundName, rhythmName, parent) {
|
|
let container = document.createElement("div");
|
|
parent.appendChild(container);
|
|
|
|
let header = document.createElement("div");
|
|
header.className = "edit-area__header";
|
|
container.appendChild(header);
|
|
|
|
let nameLabel = document.createElement("span");
|
|
header.appendChild(nameLabel);
|
|
nameLabel.innerHTML = title;
|
|
|
|
let seek = this.createButton("", header, true, "hues-icon");
|
|
header.appendChild(seek);
|
|
container._seek = seek;
|
|
|
|
let beatCount = document.createElement("span");
|
|
header.appendChild(beatCount);
|
|
beatCount.className = "edit-area__beat-count";
|
|
beatCount.textContent = "0 beats";
|
|
container._lockedBtn = this.createButton("", header, false, "hues-icon");
|
|
container._lockedBtn.addEventListener("click", () => {
|
|
if(container._locked) {
|
|
this.setLocked(container, 0);
|
|
} else {
|
|
let textLen = this.getText(container).length;
|
|
this.setLocked(container, textLen);
|
|
}
|
|
});
|
|
|
|
let rightHeader = document.createElement("span");
|
|
rightHeader.className = "edit-area__header__right";
|
|
header.appendChild(rightHeader);
|
|
|
|
container._halveBtn = this.createButton("Halve", rightHeader, true);
|
|
container._halveBtn.addEventListener("click", this.halveBeats.bind(this, container));
|
|
container._doubleBtn = this.createButton("Double", rightHeader, true);
|
|
container._doubleBtn.addEventListener("click", this.doubleBeats.bind(this, container));
|
|
|
|
let fileInput = document.createElement("input");
|
|
fileInput.type ="file";
|
|
fileInput.accept=".mp3, .wav, .ogg";
|
|
fileInput.multiple = false;
|
|
fileInput.onchange = this.loadAudio.bind(this, container);
|
|
let load = this.createButton("Load " + title.replace(/ /g,""), rightHeader);
|
|
load.addEventListener("click", () => {fileInput.click();});
|
|
|
|
container._removeBtn = this.createButton("Remove", rightHeader, true);
|
|
container._removeBtn.addEventListener("click", this.removeAudio.bind(this, container));
|
|
|
|
let editBox = document.createElement("div");
|
|
editBox.className = "edit-area__box";
|
|
let beatmap = document.createElement("div");
|
|
beatmap.className = "edit-area__beatmap";
|
|
beatmap.contentEditable = true;
|
|
beatmap.spellcheck = false;
|
|
beatmap.oninput = this.textUpdated.bind(this, container);
|
|
beatmap.oncontextmenu = this.rightClick.bind(this, container);
|
|
|
|
let beatHilight = document.createElement("div");
|
|
beatHilight.className = "beat-hilight";
|
|
|
|
editBox.appendChild(beatHilight);
|
|
editBox.appendChild(beatmap);
|
|
container.appendChild(editBox);
|
|
|
|
container._header = header;
|
|
container._beatCount = beatCount;
|
|
container._box = editBox;
|
|
container._beatmap = beatmap;
|
|
container._hilight = beatHilight;
|
|
container._fileInput = fileInput;
|
|
|
|
container._sound = soundName;
|
|
container._rhythm = rhythmName;
|
|
|
|
// Are we in insert mode? Default = no
|
|
container._locked = 0;
|
|
|
|
return container;
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateControls = function() {
|
|
let controls = document.createElement("div");
|
|
controls.className = "edit__controls";
|
|
this.root.appendChild(controls);
|
|
|
|
let changeRate = function(change) {
|
|
let rate = this.core.soundManager.playbackRate;
|
|
rate += change;
|
|
this.core.soundManager.setRate(rate);
|
|
// In case it gets clamped, check
|
|
let newRate = this.core.soundManager.playbackRate;
|
|
playRateLab.textContent = newRate.toFixed(2) + "x";
|
|
};
|
|
|
|
let speedControl = document.createElement("div");
|
|
controls.appendChild(speedControl);
|
|
|
|
// BACKWARD
|
|
let speedDown = this.createButton("", speedControl, false, "hues-icon");
|
|
speedDown.addEventListener("click", changeRate.bind(this, -0.25));
|
|
// FORWARD
|
|
let speedUp = this.createButton("", speedControl, false, "hues-icon");
|
|
speedUp.addEventListener("click", changeRate.bind(this, 0.25));
|
|
|
|
let playRateLab = document.createElement("span");
|
|
playRateLab.className = "settings-individual";
|
|
playRateLab.textContent = "1.00x";
|
|
speedControl.appendChild(playRateLab);
|
|
|
|
let wrapControl = document.createElement("div");
|
|
controls.appendChild(wrapControl);
|
|
|
|
let wrapLab = document.createElement("span");
|
|
wrapLab.className = "settings-individual";
|
|
wrapLab.textContent = "New line at beat ";
|
|
wrapControl.appendChild(wrapLab);
|
|
|
|
let wrapAt = document.createElement("input");
|
|
wrapAt.className = "settings-input";
|
|
wrapAt.value = this.wrapAt;
|
|
wrapAt.type = "text";
|
|
wrapAt.oninput = () => {
|
|
wrapAt.value = wrapAt.value.replace(/\D/g,'');
|
|
if(wrapAt.value === "" || wrapAt.value < 1) {
|
|
wrapAt.value = "";
|
|
return;
|
|
}
|
|
this.wrapAt = parseInt(wrapAt.value);
|
|
this.reflow(this.buildEdit, this.song.buildupRhythm);
|
|
this.reflow(this.loopEdit, this.song.rhythm);
|
|
|
|
};
|
|
wrapControl.appendChild(wrapAt);
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateVisualiser = function() {
|
|
let wave = document.createElement("canvas");
|
|
wave.className = "waveform";
|
|
wave.height = WAVE_HEIGHT_PIXELS;
|
|
this.root.appendChild(wave);
|
|
this.waveCanvas = wave;
|
|
this.waveContext = wave.getContext("2d");
|
|
|
|
this.core.addEventListener("frame", this.drawWave.bind(this));
|
|
};
|
|
|
|
HuesEditor.prototype.rightClick = function(editor, event) {
|
|
if(!this.linked) {
|
|
return;
|
|
}
|
|
// If the right click is also a focus event, caret doesn't move, so we have to use coords
|
|
let coords = this.getTextCoords(event);
|
|
|
|
if(coords.x > this.wrapAt)
|
|
return true;
|
|
|
|
let caret = coords.y * this.wrapAt + coords.x;
|
|
let totalLen = this.getText(editor).length;
|
|
if(caret > totalLen)
|
|
return true;
|
|
|
|
// in case of focus event
|
|
this.setCaret(editor._beatmap, caret);
|
|
let percent = caret / totalLen;
|
|
let seekTime = 0;
|
|
if(editor == this.loopEdit) { // loop
|
|
seekTime = this.core.soundManager.loopLength * percent;
|
|
} else { // build
|
|
let bLen = this.core.soundManager.buildLength;
|
|
seekTime = -bLen + bLen * percent;
|
|
}
|
|
this.core.soundManager.seek(seekTime);
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
};
|
|
|
|
HuesEditor.prototype.getTextCoords = function(event) {
|
|
// http://stackoverflow.com/a/10816667
|
|
let el = event.target,
|
|
x = 0,
|
|
y = 0;
|
|
|
|
while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) {
|
|
x += el.offsetLeft - el.scrollLeft;
|
|
y += el.offsetTop - el.scrollTop;
|
|
el = el.offsetParent;
|
|
}
|
|
|
|
x = Math.floor((event.clientX - x) / this.hilightWidth);
|
|
y = Math.floor((event.clientY - y) / this.hilightHeight);
|
|
|
|
return {x: x, y: y};
|
|
};
|
|
|
|
HuesEditor.prototype.textUpdated = function(editor) {
|
|
if(!this.song || !this.song[editor._sound]) {
|
|
this.reflow(editor, "");
|
|
return;
|
|
}
|
|
// Space at start of line is nonbreaking, get it with \u00a0
|
|
let input = editor._beatmap.textContent.replace(/ |\u00a0/g, "");
|
|
if(input.length === 0) {
|
|
input = ".";
|
|
}
|
|
this.setText(editor, input);
|
|
};
|
|
|
|
HuesEditor.prototype.getText = function(editor) {
|
|
if(!this.song || !this.song[editor._rhythm]) {
|
|
return "";
|
|
} else {
|
|
return this.song[editor._rhythm];
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.setText = function(editor, text, caretFromEnd) {
|
|
if(!this.song || !this.song[editor._sound]) {
|
|
this.reflow(editor, "");
|
|
return;
|
|
}
|
|
let commitUndo = false;
|
|
let caret = caretFromEnd ? text.length : this.getCaret(editor._beatmap);
|
|
if(editor._locked) {
|
|
caret = Math.min(editor._locked, caret);
|
|
if(text.length > editor._locked) {
|
|
// Works for pastes too! Removes the different between sizes from the caret position
|
|
text = text.slice(0, caret) + text.slice(caret + (text.length - editor._locked), text.length);
|
|
} else {
|
|
while(text.length < editor._locked) {
|
|
text += ".";
|
|
}
|
|
}
|
|
// time to scale things to fit
|
|
} else if(!this.song.independentBuild && this.song.buildupRhythm && this.song.rhythm) {
|
|
let ratio;
|
|
if(editor == this.loopEdit) {
|
|
ratio = this.core.soundManager.loopLength / this.core.soundManager.buildLength;
|
|
} else {
|
|
ratio = this.core.soundManager.buildLength / this.core.soundManager.loopLength;
|
|
}
|
|
let newLen = Math.round(text.length / ratio);
|
|
// We've tried to make the other map impossibly short, force us to be longer
|
|
while(newLen === 0) {
|
|
text += ".";
|
|
newLen = Math.round(text.length / ratio);
|
|
}
|
|
let otherMap = this.getOther(editor);
|
|
let wasLocked = otherMap._locked;
|
|
// clamp the length
|
|
otherMap._locked = newLen;
|
|
// Make undos also sync
|
|
this.batchUndo();
|
|
commitUndo = true;
|
|
// avoid infinite loop
|
|
this.song.independentBuild = true;
|
|
// Use setText to update undo state and fill/clamp beats
|
|
this.setText(otherMap, this.song[otherMap._rhythm], true);
|
|
// Restore
|
|
this.song.independentBuild = false;
|
|
// Otherwise we'll lose the new length on the next edit
|
|
if(!wasLocked) {
|
|
otherMap._locked = 0;
|
|
}
|
|
// Fix the buttons
|
|
this.updateHalveDoubleButtons(otherMap);
|
|
}
|
|
this.pushUndo(editor._rhythm, editor, this.song[editor._rhythm], text);
|
|
// If we were linked, commit our 2 edits as 1 undo state
|
|
if(commitUndo) {
|
|
this.commitUndo();
|
|
}
|
|
// Make sure you can't accidentally close the tab
|
|
window.onbeforeunload = this.confirmLeave;
|
|
this.song[editor._rhythm] = text;
|
|
this.reflow(editor, this.song[editor._rhythm]);
|
|
this.setCaret(editor._beatmap, caret);
|
|
this.updateHalveDoubleButtons(editor);
|
|
|
|
this.core.updateBeatLength();
|
|
// We may have to go backwards in time
|
|
this.core.recalcBeatIndex();
|
|
this.updateInfo();
|
|
};
|
|
|
|
HuesEditor.prototype.getCaret = function(editable) {
|
|
let caret = 0;
|
|
let sel = window.getSelection();
|
|
if (sel.rangeCount) {
|
|
let range = sel.getRangeAt(0);
|
|
// <br> elements are empty, and pastes do weird things.
|
|
// So don't go up in multiples of 2 for getCaret
|
|
for(let i = 0; i < editable.childNodes.length; i++) {
|
|
if (range.commonAncestorContainer == editable.childNodes[i]) {
|
|
caret += range.endOffset;
|
|
return caret;
|
|
} else {
|
|
caret += editable.childNodes[i].textContent.length;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
HuesEditor.prototype.setCaret = function(editable, caret) {
|
|
let range = document.createRange();
|
|
let sel = window.getSelection();
|
|
// <br> elements mean children go up in multiples of 2
|
|
for(let i = 0; i < editable.childNodes.length; i+= 2) {
|
|
let textLen = editable.childNodes[i].textContent.length;
|
|
if(caret > textLen) {
|
|
caret -= textLen;
|
|
} else {
|
|
range.setStart(editable.childNodes[i], caret);
|
|
range.collapse(true);
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.setLocked = function(editor, locked) {
|
|
editor._locked = locked;
|
|
if(locked) {
|
|
editor._lockedBtn.innerHTML = ""; // LOCKED
|
|
} else {
|
|
editor._lockedBtn.innerHTML = ""; // UNLOCKED
|
|
}
|
|
// Synchronise locks when lengths are linked
|
|
if(!this.song.independentBuild) {
|
|
let other = this.getOther(editor);
|
|
let otherLock = locked ? this.getText(other).length : 0;
|
|
this.song.independentBuild = true;
|
|
this.setLocked(other, otherLock);
|
|
this.song.independentBuild = false;
|
|
}
|
|
this.updateHalveDoubleButtons(editor);
|
|
};
|
|
|
|
HuesEditor.prototype.updateWaveform = function() {
|
|
if(this.buildWaveBuff != this.core.soundManager.buildup) {
|
|
this.buildWaveBuff = this.core.soundManager.buildup;
|
|
this.buildWave = this.renderWave(this.buildWaveBuff, this.core.soundManager.buildLength);
|
|
}
|
|
if(this.loopWaveBuff != this.core.soundManager.loop) {
|
|
this.loopWaveBuff = this.core.soundManager.loop;
|
|
this.loopWave = this.renderWave(this.loopWaveBuff, this.core.soundManager.loopLength);
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.renderWave = function(buffer, length) {
|
|
if(!buffer) {
|
|
return null;
|
|
}
|
|
// The individual wave section
|
|
let wave = document.createElement("canvas");
|
|
let waveContext = wave.getContext("2d");
|
|
|
|
wave.height = WAVE_HEIGHT_PIXELS;
|
|
wave.width = Math.floor(WAVE_PIXELS_PER_SECOND * length);
|
|
|
|
let samplesPerPixel = Math.floor(buffer.sampleRate / WAVE_PIXELS_PER_SECOND);
|
|
let waveData = [];
|
|
for(let i = 0; i < buffer.numberOfChannels; i++) {
|
|
waveData.push(buffer.getChannelData(i));
|
|
}
|
|
let channels = buffer.numberOfChannels;
|
|
// Half pixel offset makes things look crisp
|
|
let pixel = 0.5;
|
|
let halfHeight = WAVE_HEIGHT_PIXELS/2;
|
|
for(let i = 0; i < buffer.length; i += samplesPerPixel) {
|
|
let min = 0, max = 0, avgHi = 0, avgLo = 0;
|
|
let j;
|
|
for(j = 0; j < samplesPerPixel && i + j < buffer.length; j++) {
|
|
for(let chan = 0; chan < channels; chan++) {
|
|
let sample = waveData[chan][i+j];
|
|
if(sample > 0) {
|
|
avgHi += sample;
|
|
} else {
|
|
avgLo += sample;
|
|
}
|
|
if(sample > max) max = sample;
|
|
if(sample < min) min = sample;
|
|
}
|
|
}
|
|
let maxPix = Math.floor(halfHeight + max * halfHeight);
|
|
// Min is negative, addition is correct
|
|
let minPix = Math.floor(halfHeight + min * halfHeight);
|
|
waveContext.strokeStyle = "black";
|
|
waveContext.globalAlpha = "1";
|
|
waveContext.beginPath();
|
|
waveContext.moveTo(pixel, maxPix);
|
|
waveContext.lineTo(pixel, minPix);
|
|
waveContext.stroke();
|
|
|
|
// Draw the average too, gives a better feel for the wave
|
|
avgHi /= j * channels;
|
|
avgLo /= j * channels;
|
|
let maxAvg = Math.floor(halfHeight + avgHi * halfHeight);
|
|
let minAvg = Math.floor(halfHeight + avgLo * halfHeight);
|
|
waveContext.strokeStyle = "white";
|
|
waveContext.globalAlpha = "0.5";
|
|
waveContext.beginPath();
|
|
waveContext.moveTo(pixel, maxAvg);
|
|
waveContext.lineTo(pixel, minAvg);
|
|
waveContext.stroke();
|
|
|
|
pixel+=1;
|
|
}
|
|
|
|
return wave;
|
|
};
|
|
|
|
HuesEditor.prototype.drawWave = function() {
|
|
if((!this.buildWave && !this.loopWave) || !this.linked)
|
|
return;
|
|
|
|
let width = this.waveCanvas.width;
|
|
let now = this.core.soundManager.currentTime();
|
|
let timespan = width / WAVE_PIXELS_PER_SECOND / 2;
|
|
let minTime = now - timespan;
|
|
let maxTime = now + timespan;
|
|
|
|
let bLen = this.core.soundManager.buildLength;
|
|
let loopLen = this.core.soundManager.loopLength;
|
|
|
|
let drawTime, drawOffset;
|
|
if(bLen) {
|
|
drawTime = Math.max(minTime, -bLen);
|
|
} else {
|
|
drawTime = Math.max(minTime, 0);
|
|
}
|
|
// drawOffset is "pixels from the left"
|
|
drawOffset = Math.floor((drawTime - minTime) * WAVE_PIXELS_PER_SECOND);
|
|
|
|
this.waveContext.clearRect(0, 0, width, WAVE_HEIGHT_PIXELS);
|
|
|
|
if(this.buildWave && bLen && minTime < 0) {
|
|
// Bit of legwork to convert negative to positive
|
|
let waveOffset = Math.floor((1 - drawTime / -bLen) * (this.buildWave.width-1));
|
|
try {
|
|
drawOffset = this.drawOneWave(this.buildWave, waveOffset, drawOffset, width);
|
|
} catch (err) {
|
|
console.log(this.waveCanvas);
|
|
}
|
|
// If there's more to draw after the build, it'll be from the start of the wave
|
|
drawTime = 0;
|
|
}
|
|
|
|
let loopPoints = [];
|
|
if(this.loopWave && loopLen && maxTime > 0) {
|
|
while(drawOffset < width) {
|
|
if(drawTime === 0) {
|
|
loopPoints.push(drawOffset);
|
|
}
|
|
|
|
let waveOffset = Math.floor((drawTime / loopLen) * (this.loopWave.width-1));
|
|
drawOffset = this.drawOneWave(this.loopWave, waveOffset, drawOffset, width);
|
|
// If we're drawing more than 1 loop it's starting at 0
|
|
drawTime = 0;
|
|
}
|
|
}
|
|
|
|
// trackbar
|
|
this.drawWaveBar("red", width/2);
|
|
// Signify loop point with a green bar, drawing over the wave
|
|
for(let point of loopPoints) {
|
|
this.drawWaveBar("green", point);
|
|
}
|
|
};
|
|
|
|
HuesEditor.prototype.drawOneWave = function(wave, waveOffset, drawOffset, width) {
|
|
let drawWidth = Math.min(width - drawOffset, wave.width - waveOffset);
|
|
this.waveContext.drawImage(wave,
|
|
waveOffset, 0, // source x/y
|
|
drawWidth, WAVE_HEIGHT_PIXELS, // source width/height
|
|
drawOffset, 0, // dest x/y
|
|
drawWidth, WAVE_HEIGHT_PIXELS); // dest width/height
|
|
return drawOffset + drawWidth;
|
|
};
|
|
|
|
HuesEditor.prototype.drawWaveBar = function(colour, offset) {
|
|
this.waveContext.strokeStyle = colour;
|
|
this.waveContext.lineWidth = 2;
|
|
this.waveContext.beginPath();
|
|
this.waveContext.moveTo(offset, 0);
|
|
this.waveContext.lineTo(offset, WAVE_HEIGHT_PIXELS);
|
|
this.waveContext.stroke();
|
|
};
|
|
|
|
HuesEditor.prototype.confirmLeave = function() {
|
|
return "Unsaved beatmap - leave anyway?";
|
|
};
|
|
|
|
HuesEditor.prototype.alert = function(msg) {
|
|
this.statusMsg.classList.remove("editor__status-msg--fade");
|
|
this.statusMsg.textContent = msg;
|
|
// Trigger a reflow and thus restart the animation
|
|
var useless = this.statusMsg.offsetWidth;
|
|
this.statusMsg.classList.add("editor__status-msg--fade");
|
|
};
|
|
|
|
HuesEditor.prototype.generateXML = function() {
|
|
if(!this.song) {
|
|
return null;
|
|
}
|
|
// Yes, this is just a bunch of strings. Simple XML, simple method.
|
|
|
|
let result = " <song name=\"" + this.song.name + "\">\n";
|
|
result += " <title>" + this.song.title + "</title>\n";
|
|
if(this.song.source) {
|
|
result += " <source>" + this.song.source + "</source>\n";
|
|
}
|
|
result += " <rhythm>" + this.song.rhythm + "</rhythm>\n";
|
|
if(this.song.buildup) {
|
|
result += " <buildup>" + this.song.buildupName + "</buildup>\n";
|
|
result += " <buildupRhythm>" + this.song.buildupRhythm + "</buildupRhythm>\n";
|
|
if(this.song.independentBuild) {
|
|
result += " <independentBuild>true</independentBuild>\n";
|
|
}
|
|
}
|
|
result += " </song>\n";
|
|
return result;
|
|
};
|
|
|
|
HuesEditor.prototype.saveXML = function() {
|
|
let xml = this.generateXML();
|
|
if(!xml) {
|
|
return;
|
|
}
|
|
let result = "<songs>\n";
|
|
result += xml;
|
|
result += "</songs>\n";
|
|
|
|
// http://stackoverflow.com/a/18197341
|
|
let element = document.createElement('a');
|
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(result));
|
|
element.setAttribute('download', "0x40Hues - " + this.song.name + ".xml");
|
|
|
|
element.style.display = 'none';
|
|
document.body.appendChild(element);
|
|
|
|
element.click();
|
|
|
|
document.body.removeChild(element);
|
|
|
|
window.onbeforeunload = null;
|
|
};
|
|
|
|
// http://stackoverflow.com/a/30810322
|
|
HuesEditor.prototype.copyXML = function() {
|
|
let text = this.generateXML();
|
|
|
|
// Clicking when disabled
|
|
if(!text) {
|
|
return;
|
|
}
|
|
|
|
let textArea = document.createElement("textarea");
|
|
textArea.className = "copybox";
|
|
|
|
textArea.value = text;
|
|
|
|
document.body.appendChild(textArea);
|
|
|
|
textArea.select();
|
|
|
|
let success;
|
|
|
|
try {
|
|
success = document.execCommand('copy');
|
|
} catch (err) {
|
|
success = false;
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
if(success) {
|
|
this.alert("Beatmap XML copied to clipboard!");
|
|
} else {
|
|
this.alert("Copy failed! Try saving instead");
|
|
}
|
|
};
|
|
|
|
window.HuesEditor = HuesEditor;
|
|
|
|
})(window, document); |