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.
922 lines
31 KiB
922 lines
31 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";
|
|
|
|
function HuesEditor(core) {
|
|
this.buildEditSize = 80; // pixels, including header
|
|
this.buildEdit = null;
|
|
this.loopEdit = null;
|
|
this.editArea = null;
|
|
this.wrapAt = 16;
|
|
|
|
this.hilightWidth = 0;
|
|
this.hilightHeight = 0;
|
|
|
|
this.undoBuffer = [];
|
|
this.redoBuffer = [];
|
|
|
|
// for storing respacks created with "new"
|
|
this.respack = null;
|
|
// when we're actually following the playing song
|
|
this.linked = false;
|
|
|
|
this.core = core;
|
|
this.root = document.getElementById("huesEditor");
|
|
if(!this.root) {
|
|
return;
|
|
}
|
|
if(!core.settings.defaults.noUI) {
|
|
this.initUI();
|
|
core.addEventListener("beat", this.onBeat.bind(this));
|
|
core.addEventListener("newsong", this.onNewSong.bind(this));
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.initUI = function() {
|
|
var titleButtons = document.createElement("div");
|
|
titleButtons.id = "edit-titlebuttons";
|
|
this.root.appendChild(titleButtons);
|
|
this.saveBtn = this.createButton("Save XML", titleButtons, true);
|
|
this.saveBtn.onclick = this.saveXML.bind(this);
|
|
this.copyBtn = this.createButton("Copy XML", titleButtons, true);
|
|
this.copyBtn.onclick = this.copyXML.bind(this);
|
|
this.undoBtn = this.createButton("Undo", titleButtons, true);
|
|
this.redoBtn = this.createButton("Redo", titleButtons, true);
|
|
var help = this.createButton("Help?", titleButtons);
|
|
help.style.backgroundColor = "rgba(0,160,0,0.3)";
|
|
help.onclick = function() {
|
|
window.open("http://0x40hues.blogspot.com/p/0x40-hues-creation-tutorial.html", '_blank');
|
|
};
|
|
|
|
this.topBar = document.createElement("div");
|
|
this.topBar.id = "edit-topbar";
|
|
this.root.appendChild(this.topBar);
|
|
|
|
this.uiCreateInfo();
|
|
this.uiCreateImport();
|
|
this.root.appendChild(document.createElement("hr"));
|
|
this.uiCreateEditArea();
|
|
this.uiCreateControls();
|
|
this.uiCreateVisualiser();
|
|
|
|
window.addEventListener('resize', this.resize.bind(this));
|
|
// Fix Chrome rendering - redraw on tab load
|
|
document.getElementById("tab-editor").addEventListener("change", this.resize.bind(this));
|
|
this.resize();
|
|
};
|
|
|
|
HuesEditor.prototype.resize = function(noHilightCalc) {
|
|
this.root.style.height = (window.innerHeight - 200) + "px";
|
|
var boxHeight = this.editArea.offsetHeight;
|
|
var bHeadHeight = this.buildEdit._header.offsetHeight;
|
|
var lHeadHeight = this.loopEdit._header.offsetHeight;
|
|
var handleHeight = this.resizeHandle.offsetHeight;
|
|
var minHeight = bHeadHeight;
|
|
var maxHeight = boxHeight - handleHeight - lHeadHeight - bHeadHeight;
|
|
var buildHeight = Math.min(maxHeight, Math.max(minHeight, this.buildEditSize - handleHeight));
|
|
this.buildEdit.style.height = buildHeight + "px";
|
|
this.buildEdit._box.style.height = (buildHeight - bHeadHeight) + "px";
|
|
var 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) {
|
|
var hilight = document.createElement("div");
|
|
hilight.className = "beat-hilight";
|
|
hilight.innerHTML = "█";
|
|
this.root.appendChild(hilight);
|
|
this.hilightWidth = hilight.clientWidth;
|
|
this.hilightHeight = hilight.clientHeight;
|
|
this.editorWidth = this.loopEdit._beatmap.clientWidth;
|
|
this.root.removeChild(hilight);
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.createTextInput = function(label, id, subtitle, parent) {
|
|
var div = document.createElement("div");
|
|
div.className = "edit-label";
|
|
var caption = document.createElement("label");
|
|
caption.innerHTML = label;
|
|
caption.htmlFor = id;
|
|
div.appendChild(caption);
|
|
var container = document.createElement("span");
|
|
container.className = "edit-textbox-container";
|
|
var input = document.createElement("input");
|
|
input.className = "edit-textbox";
|
|
input.type = "text";
|
|
input.id = id;
|
|
input.value = subtitle;
|
|
container.appendChild(input);
|
|
div.appendChild(container);
|
|
|
|
parent.appendChild(div);
|
|
|
|
return input;
|
|
}
|
|
|
|
HuesEditor.prototype.createButton = function(label, parent, disabled, extraClass) {
|
|
var button = document.createElement("span");
|
|
button.className = "hues-button";
|
|
if(disabled) {
|
|
button.className += " disabled";
|
|
}
|
|
if(extraClass) {
|
|
button.className += " " + extraClass;
|
|
}
|
|
button.innerHTML = label.toUpperCase();
|
|
parent.appendChild(button);
|
|
return button;
|
|
}
|
|
|
|
HuesEditor.prototype.uiCreateInfo = function() {
|
|
var info = document.createElement("div");
|
|
this.topBar.appendChild(info);
|
|
info.id = "edit-info";
|
|
|
|
var 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:", "edit-title", "Song name", info);
|
|
this.title.oninput = songUpdate.bind(this, "title");
|
|
this.source = this.createTextInput("Link: ", "edit-source", "Source link", info);
|
|
this.source.oninput = songUpdate.bind(this, "source");
|
|
};
|
|
|
|
HuesEditor.prototype.onNewSong = function(song) {
|
|
if(this.linked) {
|
|
if(song == this.song) {
|
|
// Because you can "edit current" before it loads
|
|
this.updateInfo();
|
|
} else {
|
|
this.linked = false;
|
|
// Clear beat hilight
|
|
this.buildEdit._hilight.innerHTML = "█";
|
|
this.loopEdit._hilight.innerHTML = "█";
|
|
this.buildEdit._hilight.className = "beat-hilight hidden";
|
|
this.loopEdit._hilight.className = "beat-hilight hidden";
|
|
}
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.onBeat = function(map, index) {
|
|
if(!this.song || this.core.currentSong != this.song) {
|
|
return;
|
|
}
|
|
var editor;
|
|
if(index < 0) {
|
|
index += this.core.currentSong.buildupRhythm.length;
|
|
editor = this.buildEdit;
|
|
this.loopEdit._hilight.className = "beat-hilight hidden";
|
|
} else {
|
|
editor = this.loopEdit;
|
|
if(this.song.buildup) {
|
|
this.buildEdit._hilight.className = "beat-hilight hidden";
|
|
}
|
|
}
|
|
editor._hilight.className = "beat-hilight";
|
|
var offsetX = index % editor._breakAt;
|
|
var offsetY = Math.floor(index / editor._breakAt);
|
|
// Not computing width/height here due to Chrome bug
|
|
editor._hilight.style.left = (offsetX * this.hilightWidth) + "px";
|
|
editor._hilight.style.top = (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 = "█";
|
|
}
|
|
var charsPerLine = Math.floor(this.editorWidth / this.hilightWidth);
|
|
// if it's too long to wrap, just give up
|
|
var wrap = Math.min(this.wrapAt, charsPerLine);
|
|
charsPerLine -= charsPerLine % wrap;
|
|
editor._beatCount.textContent = map.length + " beats";
|
|
// http://stackoverflow.com/a/27012001
|
|
var regex = new RegExp("(.{" + charsPerLine + "})", "g");
|
|
editor._beatmap.innerHTML = map.replace(regex, "$1<br />");
|
|
editor._breakAt = charsPerLine;
|
|
}
|
|
|
|
HuesEditor.prototype.loadAudio = function(editor) {
|
|
if(editor._fileInput.files.length < 1) {
|
|
return;
|
|
}
|
|
// Disable load button TODO
|
|
var file = editor._fileInput.files[0];
|
|
// load audio
|
|
this.blobToArrayBuffer(file)
|
|
.then(buffer => {
|
|
// If first load, this makes fresh, gets the core synced up
|
|
this.newSong(this.song);
|
|
|
|
this.song[editor._sound] = buffer;
|
|
// Save filename for XML export
|
|
var 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(() => {
|
|
this.updateInfo();
|
|
this.reflow(editor, this.getText(editor));
|
|
this.core.updateBeatLength();
|
|
// We may have to go backwards in time
|
|
this.core.recalcBeatIndex();
|
|
}).catch(error => {
|
|
console.log(error);
|
|
alert("Couldn't load song! Is it a LAME encoded MP3?");
|
|
});
|
|
}
|
|
|
|
HuesEditor.prototype.blobToArrayBuffer = function(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
var 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};
|
|
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 hidden";
|
|
this.loopEdit._hilight.className = "beat-hilight hidden";
|
|
|
|
// Clear helpful glows
|
|
this.newSongBtn.classList.remove("glow");
|
|
this.fromSongBtn.classList.remove("glow");
|
|
|
|
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;
|
|
|
|
// Unlock beatmap lengths
|
|
this.setLocked(this.buildEdit, 0);
|
|
this.setLocked(this.loopEdit, 0);
|
|
|
|
this.linked = true;
|
|
this.updateInfo();
|
|
}
|
|
|
|
HuesEditor.prototype.updateInfo = function() {
|
|
if(!this.linked) {
|
|
return;
|
|
}
|
|
|
|
var loopLen = this.core.soundManager.loopLength;
|
|
var buildLen = this.core.soundManager.buildLength;
|
|
var 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);
|
|
|
|
// Avoid a bunch of nested elses
|
|
this.seekStart.className = "hues-button disabled";
|
|
this.seekLoop.className = "hues-button disabled";
|
|
this.saveBtn.className = "hues-button disabled";
|
|
this.copyBtn.className = "hues-button disabled";
|
|
this.buildEdit._removeBtn.className = "hues-button disabled";
|
|
this.loopEdit._removeBtn.className = "hues-button disabled";
|
|
|
|
if(this.song) {
|
|
this.saveBtn.className = "hues-button";
|
|
this.copyBtn.className = "hues-button";
|
|
if(this.song.sound) {
|
|
this.seekLoop.className = "hues-button";
|
|
this.loopEdit._removeBtn.className = "hues-button";
|
|
if(this.song.buildup) {
|
|
this.seekStart.className = "hues-button";
|
|
this.buildEdit._removeBtn.className = "hues-button";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.pushUndo = function(name, editor, oldText, newText) {
|
|
if(oldText == newText) {
|
|
return;
|
|
}
|
|
this.redoBuffer = [];
|
|
|
|
this.undoBuffer.push({songVar: name, editor: editor, text: oldText});
|
|
while(this.undoBuffer.length > 50) {
|
|
this.undoBuffer.pop();
|
|
}
|
|
this.updateUndoUI();
|
|
}
|
|
|
|
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
|
|
var fromData = from.pop();
|
|
// Make restore from current
|
|
to.push({songVar: fromData.songVar, editor: fromData.editor, text: this.song[fromData.songVar]});
|
|
// Restore to editor
|
|
this.song[fromData.songVar] = fromData.text;
|
|
this.reflow(fromData.editor, this.song[fromData.songVar]);
|
|
this.updateUndoUI();
|
|
this.updateHalveDoubleButtons();
|
|
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 disabled";
|
|
this.undoBtn.onclick = null;
|
|
this.redoBtn.className = "hues-button disabled";
|
|
this.redoBtn.onclick = null;
|
|
|
|
if(this.undoBuffer.length > 0) {
|
|
this.undoBtn.classList.remove("disabled");
|
|
this.undoBtn.onclick = this.undo.bind(this);
|
|
}
|
|
if(this.redoBuffer.length > 0) {
|
|
this.redoBtn.classList.remove("disabled");
|
|
this.redoBtn.onclick = this.redo.bind(this);
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.halveBeats = function(editor) {
|
|
if(!this.song || this.getText(editor).length < 2) {
|
|
return;
|
|
}
|
|
this.setText(editor, this.song[editor._rhythm].replace(/(.)./g, "$1"));
|
|
}
|
|
|
|
HuesEditor.prototype.doubleBeats = function(editor) {
|
|
if(!this.song || this.getText(editor).length == 0) {
|
|
return;
|
|
}
|
|
this.setText(editor, this.song[editor._rhythm].replace(/(.)/g, "$1."));
|
|
}
|
|
|
|
HuesEditor.prototype.uiCreateImport = function() {
|
|
var imports = document.createElement("div");
|
|
this.topBar.appendChild(imports);
|
|
imports.id = "edit-imports";
|
|
|
|
var songEdits = document.createElement("div");
|
|
imports.appendChild(songEdits);
|
|
var newSongBtn = this.createButton("New song", songEdits, false, "glow");
|
|
newSongBtn.onclick = () => {
|
|
this.newSong();
|
|
};
|
|
this.newSongBtn = newSongBtn;
|
|
var fromSong = this.createButton("Edit current song", songEdits, false, "glow");
|
|
fromSong.onclick = () => {
|
|
if(this.core.currentSong) {
|
|
this.newSong(this.core.currentSong);
|
|
}
|
|
};
|
|
this.fromSongBtn = fromSong;
|
|
|
|
var songInfos = document.createElement("div");
|
|
songInfos.className = "settings-individual";
|
|
songInfos.id = "edit-songstats";
|
|
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) {
|
|
var container = document.createElement("div");
|
|
parent.appendChild(container);
|
|
var label = document.createElement("span");
|
|
label.textContent = name;
|
|
container.appendChild(label);
|
|
var valueSpan = document.createElement("span");
|
|
valueSpan.textContent = value;
|
|
valueSpan.className = "edit-songstat-value";
|
|
container.appendChild(valueSpan);
|
|
return valueSpan;
|
|
}
|
|
|
|
HuesEditor.prototype.uiCreateEditArea = function() {
|
|
var editArea = document.createElement("div");
|
|
this.editArea = editArea;
|
|
editArea.id = "edit-area";
|
|
this.root.appendChild(editArea);
|
|
|
|
// Lock build/loop lengths
|
|
this.timeLock = document.createElement("div");
|
|
editArea.appendChild(this.timeLock);
|
|
this.timeLock.id = "edit-timelock";
|
|
this.timeLock.className = "hues-icon";
|
|
// CHAIN, use  for CHAIN-BROKEN
|
|
this.createButton("", this.timeLock)
|
|
this.buildEdit = this.uiCreateSingleEditor("Buildup ", "buildup", "buildupRhythm", "edit-build", editArea);
|
|
|
|
// drag handle
|
|
var handleContainer = document.createElement("div");
|
|
handleContainer.id = "edit-resize-handle-container";
|
|
editArea.appendChild(handleContainer);
|
|
var handle = document.createElement("div");
|
|
handle.id = 'edit-resize-handle';
|
|
handle.className = 'hues-icon';
|
|
handle.innerHTML = ""; // DRAG HANDLE
|
|
handleContainer.appendChild(handle);
|
|
this.resizeHandle = handleContainer;
|
|
|
|
handleContainer.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
var editTop = this.editArea.getBoundingClientRect().top;
|
|
var handleSize = this.resizeHandle.clientHeight;
|
|
|
|
var resizer = (e) => {
|
|
this.buildEditSize = Math.floor(e.clientY - editTop + handleSize/2);
|
|
this.resize(true);
|
|
};
|
|
|
|
var 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", "edit-loop", editArea);
|
|
|
|
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 />\
|
|
[<< START] starts the song from the beginning.<br />\
|
|
[< LOOP] starts the song from the start of the loop.<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, id, parent) {
|
|
var container = document.createElement("div");
|
|
container.id = id;
|
|
parent.appendChild(container);
|
|
|
|
var header = document.createElement("div");
|
|
header.className = "edit-area-header";
|
|
container.appendChild(header);
|
|
|
|
var nameLabel = document.createElement("span");
|
|
header.appendChild(nameLabel);
|
|
nameLabel.innerHTML = title;
|
|
var beatCount = document.createElement("span");
|
|
header.appendChild(beatCount);
|
|
beatCount.className = "beat-count";
|
|
beatCount.textContent = "0 beats";
|
|
container._lockedBtn = this.createButton("", header, false, "hues-icon");
|
|
container._lockedBtn.onclick = () => {
|
|
if(container._locked) {
|
|
this.setLocked(container, 0);
|
|
} else {
|
|
var textLen = this.getText(container).length;
|
|
this.setLocked(container, textLen);
|
|
}
|
|
};
|
|
|
|
var rightHeader = document.createElement("span");
|
|
rightHeader.className = "edit-area-right-header";
|
|
header.appendChild(rightHeader);
|
|
|
|
container._halveBtn = this.createButton("Halve", rightHeader);
|
|
container._halveBtn.onclick = this.halveBeats.bind(this, container)
|
|
container._doubleBtn = this.createButton("Double", rightHeader);
|
|
container._doubleBtn.onclick = this.doubleBeats.bind(this, container)
|
|
|
|
var fileInput = document.createElement("input");
|
|
fileInput.type ="file";
|
|
fileInput.accept="audio/mpeg3";
|
|
fileInput.multiple = false;
|
|
fileInput.onchange = this.loadAudio.bind(this, container);
|
|
var load = this.createButton("Load " + title.replace(/ /g,""), rightHeader);
|
|
load.onclick = () => {fileInput.click()};
|
|
|
|
container._removeBtn = this.createButton("Remove", rightHeader, true);
|
|
|
|
var editBox = document.createElement("div");
|
|
editBox.className = "edit-box";
|
|
var beatmap = document.createElement("div");
|
|
beatmap.className = "beatmap";
|
|
beatmap.contentEditable = true;
|
|
beatmap.spellcheck = false;
|
|
beatmap.oninput = this.textUpdated.bind(this, container);
|
|
beatmap.oncontextmenu = this.rightClick.bind(this, container);
|
|
|
|
var 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
|
|
this.setLocked(container, 0);
|
|
|
|
return container;
|
|
}
|
|
|
|
HuesEditor.prototype.rightClick = function(editor, event) {
|
|
// We abuse the fact that right clicking moves the caret. Hooray!
|
|
var caret = this.getCaret(editor._beatmap);
|
|
var totalLen = this.getText(editor).length;
|
|
var percent = caret / totalLen;
|
|
if(caret <= totalLen) {
|
|
var seekTime = 0;
|
|
if(editor._rhythm == "rhythm") { // loop
|
|
seekTime = this.core.soundManager.loopLength * percent;
|
|
} else { // build
|
|
var bLen = this.core.soundManager.buildLength;
|
|
seekTime = -bLen + bLen * percent;
|
|
}
|
|
this.core.soundManager.seek(seekTime);
|
|
event.preventDefault();
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
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
|
|
var 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) {
|
|
if(!this.song || !this.song[editor._sound]) {
|
|
this.reflow(editor, "");
|
|
return;
|
|
}
|
|
var caret = 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 += ".";
|
|
}
|
|
}
|
|
}
|
|
this.pushUndo(editor._rhythm, editor, this.song[editor._rhythm], text);
|
|
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) {
|
|
var caret = 0;
|
|
var sel = window.getSelection();
|
|
if (sel.rangeCount) {
|
|
var 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(var 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) {
|
|
var range = document.createRange();
|
|
var sel = window.getSelection();
|
|
// <br> elements mean children go up in multiples of 2
|
|
for(var i = 0; i < editable.childNodes.length; i+= 2) {
|
|
var 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.updateHalveDoubleButtons = function(editor) {
|
|
editor._halveBtn.className = "hues-button disabled";
|
|
editor._doubleBtn.className = "hues-button disabled";
|
|
|
|
if(!editor._locked) {
|
|
var txtLen = this.getText(editor).length;
|
|
if(txtLen > 0) {
|
|
editor._doubleBtn.className = "hues-button";
|
|
}
|
|
if(txtLen > 1) {
|
|
editor._halveBtn.className = "hues-button";
|
|
}
|
|
}
|
|
}
|
|
|
|
HuesEditor.prototype.setLocked = function(editor, locked) {
|
|
editor._locked = locked;
|
|
if(locked) {
|
|
editor._lockedBtn.innerHTML = ""; // LOCKED
|
|
} else {
|
|
editor._lockedBtn.innerHTML = ""; // UNLOCKED
|
|
}
|
|
this.updateHalveDoubleButtons(editor);
|
|
}
|
|
|
|
HuesEditor.prototype.uiCreateControls = function() {
|
|
var controls = document.createElement("div");
|
|
controls.id = "edit-controls";
|
|
this.root.appendChild(controls);
|
|
|
|
this.seekStart = this.createButton("<< Start", controls, true);
|
|
this.seekStart.onclick = () => {
|
|
this.core.soundManager.seek(-this.core.soundManager.buildLength);
|
|
};
|
|
this.seekLoop = this.createButton("< Loop", controls, true);
|
|
this.seekLoop.onclick = () => {
|
|
this.core.soundManager.seek(0);
|
|
};
|
|
|
|
var playRateLab = document.createElement("span");
|
|
playRateLab.className = "settings-individual";
|
|
playRateLab.textContent = "1.00x";
|
|
controls.appendChild(playRateLab);
|
|
|
|
var changeRate = function(change) {
|
|
var rate = this.core.soundManager.playbackRate;
|
|
rate += change;
|
|
this.core.soundManager.setRate(rate);
|
|
// In case it gets clamped, check
|
|
var newRate = this.core.soundManager.playbackRate;
|
|
playRateLab.textContent = newRate.toFixed(2) + "x";
|
|
}
|
|
|
|
// BACKWARD
|
|
var speedDown = this.createButton("", controls, false, "hues-icon");
|
|
speedDown.onclick = changeRate.bind(this, -0.25);
|
|
// FORWARD
|
|
var speedUp = this.createButton("", controls, false, "hues-icon");
|
|
speedUp.onclick = changeRate.bind(this, 0.25);
|
|
|
|
var wrapLab = document.createElement("span");
|
|
wrapLab.className = "settings-individual";
|
|
wrapLab.textContent = "New line at beat ";
|
|
controls.appendChild(wrapLab);
|
|
|
|
var 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);
|
|
|
|
};
|
|
controls.appendChild(wrapAt);
|
|
};
|
|
|
|
HuesEditor.prototype.uiCreateVisualiser = function() {
|
|
// TODO placeholder
|
|
var waveDiv = document.createElement("div");
|
|
waveDiv.id = "edit-waveform";
|
|
this.root.appendChild(waveDiv);
|
|
};
|
|
|
|
HuesEditor.prototype.generateXML = function() {
|
|
if(!this.song) {
|
|
return null;
|
|
}
|
|
// Yes, this is just a bunch of strings. Simple XML, simple method.
|
|
|
|
var 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() {
|
|
var xml = this.generateXML();
|
|
if(!xml) {
|
|
return;
|
|
}
|
|
var result = "<songs>\n";
|
|
result += xml;
|
|
result += "</songs>\n";
|
|
|
|
// http://stackoverflow.com/a/18197341
|
|
var 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);
|
|
}
|
|
|
|
// http://stackoverflow.com/a/30810322
|
|
HuesEditor.prototype.copyXML = function() {
|
|
var text = this.generateXML();
|
|
|
|
// Clicking when disabled
|
|
if(!text) {
|
|
return;
|
|
}
|
|
|
|
var textArea = document.createElement("textarea");
|
|
textArea.id = "edit-copybox";
|
|
|
|
textArea.value = text;
|
|
|
|
document.body.appendChild(textArea);
|
|
|
|
textArea.select();
|
|
|
|
var success;
|
|
|
|
try {
|
|
success = document.execCommand('copy');
|
|
} catch (err) {
|
|
success = false;
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
if(success) {
|
|
alert("Beatmap XML copied to clipboard!");
|
|
} else {
|
|
alert("Copy failed! Try saving instead");
|
|
}
|
|
|
|
}
|
|
|
|
window.HuesEditor = HuesEditor;
|
|
|
|
})(window, document); |