From eae16ce53526ac01f219b70c0997eb1d2f73a43b Mon Sep 17 00:00:00 2001 From: William Toohey Date: Tue, 2 Feb 2016 16:39:42 +1000 Subject: [PATCH] Add beatmap editor --- fonts/HuesExtra.eot | Bin 1608 -> 2840 bytes fonts/HuesExtra.svg | 7 + fonts/HuesExtra.ttf | Bin 1444 -> 2676 bytes fonts/HuesExtra.woff | Bin 1520 -> 2752 bytes index.html | 26 +- src/css/hues-editor.css | 190 ++++++++ src/css/style.css | 61 ++- src/js/HuesCore.js | 40 +- src/js/HuesEditor.js | 949 ++++++++++++++++++++++++++++++++++++++++ src/js/HuesSettings.js | 29 +- src/js/ResourcePack.js | 10 +- src/js/SoundManager.js | 48 +- 12 files changed, 1287 insertions(+), 73 deletions(-) create mode 100644 src/css/hues-editor.css create mode 100644 src/js/HuesEditor.js diff --git a/fonts/HuesExtra.eot b/fonts/HuesExtra.eot index c64ae41baeec95104980a366c74e5c43148d170b..a5635feba0c11927758e9cb0c89ad48b76b00e29 100644 GIT binary patch delta 1527 zcmZWpU1%It6h8OfnL9I^mAy%3XV$Q_*{qorD(?R-Xte>eDq7e|mk334m$VSXO|vy_ z0tz!ANQEM97C|Mc#UD^oq)=#l5IPS&wBUo((C57Ppie$T5Xx>mcV=5Fnc@C?=YDg( zGxwW2x1yg}JTV5a8tL(hQojJ5eorxbjm7Z+0Q`ilx!7*4*b8@hy<185%;g`S1|aJI z{QaeKt+V=#*dqYiKJqF{7|^bGv-c36LVWz~^NX#>nJ7N5UqU?H zZmq9C0=5xfm3X<;KKJ>{#y!NppvH$Q=igZchAR+h$aP?h-0q(xCrG6K4S9t`uKz^{ zb#;J)eYV54VFG5M3JvJM2kr%Nm=4#s6*d}gHDKd!~GvF-=B%K zSR&*r=p7ZpGVasKB!$qSZmw9&*J`=7kz?I#p->3$zKw#D#4r?wBAQ_fK`B?6BGnSM z=(@qalRi#I`@I$m%2T9_9`IGpg6a~(b}fs3b+fe>^2H*`)dE+#x%n6Kg#u!rc$$3b zF%KSvG@inAx#ZyC;73S$hR4TEtC%WO%Ht(xwj|Grb;KnpdP;QaUcDpCWYVP5*biTK zro1WV0Mq&PPN(iC&7q8&WY0|cQ;sv`%X3Yn81qp~N@22xe`lTMv^s1j!WpH{sig&26S%{;n|GY#-}H<^m3;^kCa#pW>>U|cr3 zy|CM8++A1*ira%G^Vl?SEN(ow=^U9G`ok-1dgzn6fkX|TH07K4NN$GY*%28ei8Pz? z8R|hF4)8B^q5fv<_}?(51o+PG?=Q)T9Q&qHC9(cVW%?xB!SZ*&kfc%baNNV@ xOFBfrgva0#d;!nzzQ-? zfq|Ksan|G%Mter)$x9f$8JRbKU}R)cW(28VU^pD#>>AH+^Ob>{1 + + + + + + + \ No newline at end of file diff --git a/fonts/HuesExtra.ttf b/fonts/HuesExtra.ttf index ae6bda4cbdc805c5ea1e4358c9c0fb381f4816a6..946e462edd8248a4358348f198bcc11523190ab6 100644 GIT binary patch delta 1546 zcmZWpUx*t;82@G_`!9)P?ryUikJMf+-bSdH{|RWdCtg%+A(aa!6w!oQdq{IlY|Wi0 z?1CT_ig*b=D0j7pLM0-FLaz@(_raHf4^odl--{3We1cH2@tfVWR&IxxZ+_qW_WON1 z-_Cp&+1G6#01yEO806~9*+Ohu8lmTB?9J2dR>xeqy9q#&h#O~H7dmJ;Vqw*@=RSP* z)RkM$BHjRyKVCi4dPlt(dkjF?$Mwo8I+W||H>5s|_{r7w#f|(L`v~z##7EDaKmB4W zd@70o)XV6YYqvH!kboiL9|c}(wam?%EshFAy2?rgfpNVrdq zlkklbgiu=r81J(kHiT(dfGRYg3!lK3a1EfU$m#ivRW2PPm14m(;<{pD_nps^QZ;a9 z&~t?NylUk+f;>r&5hP`am8qIWv4F3aOIcDi4ih6zl#G?0M?zdbOccvT)oMAG;$mS#B_?8o- z;*YP_vUVezza7=XTO9K+Oul_78g=t8kt>mH&X;}9XTr0zlv~aTW==@R!RL`@ZU!hY zZ~4NC+!uK%GFDz2kJwM_TQ~x5fDN#KRlvb13eFHs3o#T?G?Q~m zWr>A^TEgT#+uwIG!0BYa*JFNqAt9ssd`V`0b_uUt%VNEH+1ktbViDzPepPz8rC0KW z0%D+enq2BI2Ofts9>H|EWZ~hE50Uh|9G}vSVyaLnPnE2Nk~k~IA)BP=N#3nH^)A@pX{gcJsQM6OscP)<&# z<}H+~6jNm!7}UFtAKKR%VKf_qe$lNn2UUW||I?~>g%HF0Zpa)u#F>J(AIzlUsdzaR zm#{kwM(7uv4p(*?je9FAesX)zWDc7H8Izmx8(k!ep6gv@bDm2UM*=lm(iC6fBDoom z7bav7B+_h(cc?3XchwJ!1K=(7e*Nv((Z3-Q^6}l>KVB6RVFw*nC9%OZHg|&UVEQ|t z3DTrHX!o$Yf(~KO;R(15U&D3yi`=81(_iR6Y??jAUJx4$G57)Sj>Lx!Hl>*xWA;7y E7m?r>lK=n! delta 305 zcmew&vV^;yfsuiMft#U$ftkU;KUm+0Ux@WMP-G7fCnV=47Kn#k>SADElmYTX(i4jd zfV2RR5288Jb1KuW9yos;$gcq6D;cSYDIC51D;XG=eSq?289)K{4a_Hj{1zZzB_p?_ zLX3ei2*_UnzMLEb`&ZAkFA3}x#(H4<-1(2* zL+mbq`iZd9*M2#+bhiC2QfkPl2-9!Io>)Q!p$^-GcC8}+Qy%;xVz&G0(MIuaJ&xDt(e`4`Ex$Oh-9B=WgH=I3*#a)Ox? z5~{yE^6X7N^4;4~SX0+TU789EjJ+ZInf(BB@Fq9_v)BdPd@W%GcieRR&CJ;HmQ4vd zMtpM6DFmxN2gh>I`yfrFOb$AZk1~Gm|LdywZtz>d{<0ch$Uz`*bov!S7HnS1ji1_1tREv0k=ND9Cu%Lcs}Q z7$JrtnqhHHsj9G$P)l08=g560eVhjSy&99{g@lYQ`HIS9b;(hup2dFkvh{_0v50bY z*_B>y{-u1OfH6=!O&)cb3r|8C4`aGqvhi@JM@V``jgOg$VyaLnkCp7%k~k}gBMwQ? zlf2t-8(nTrO__8W$I)B%q&sQvVLGqT?KZqAb0lz+?3h#Dq-{@nc$~%GYR_XHiU}!9 z_Ly9;kU%*#otm*xu2M{uabVEsx-zxY8e%pFlYZ50Fc($)%>UDBbcGND7Y59w1Dt8% z=Qxpyr{d*QTtRag3=tPb2aDUy=7YsWS=3k!@Y(NGg`hi=d+~3WOfsp|SSU)gugXtv`MYQXWC+8*>FfcG?0EI$8SUl`fS9)SGNNf*~ z&jG~(={c2YK(Ql0dmS~bM*GF%m4}k#V!E(Dj>|hf%#-c zZb=1D>j2x5aa45)u<+vC`7~~jw7-lhSV0Z--0U5!;;LX6a JnTc~BBLMCHLyQ0b diff --git a/index.html b/index.html index 7affb7f..f5fe92c 100644 --- a/index.html +++ b/index.html @@ -33,21 +33,29 @@
x
- - - - - - -
+ + + + +
+
+ + + + +
-
+
+ +
+
+
-
+

0x40 Hues of JS,

Adapted from the 0x40 Flash

diff --git a/src/css/hues-editor.css b/src/css/hues-editor.css new file mode 100644 index 0000000..aafe324 --- /dev/null +++ b/src/css/hues-editor.css @@ -0,0 +1,190 @@ +#huesEditor { + position: relative; + display: flex !important; + flex-direction: column; + max-width: calc(100% - 10px); + width: 1000px; + + min-height: 400px; + margin: 5px; + + font-size: 13px; +} + +#huesEditor > hr { + width: 100%; +} + +#edit-titlebuttons { + margin: -2px 0 2px -2px; + height: 15px; +} + +#edit-topbar { + display: flex; + height: 60px; +} + + +#edit-info { + flex-grow: 1; +} + +#edit-imports { + padding-top: 6px; +} + +#edit-songstats { + margin-top: 5px; + margin-left: -8px; +} + +.edit-songstat-value { + position: absolute; + right: 2px; +} + +.edit-label { + display: flex; + margin: 12px auto; +} + +.edit-textbox-container { + display: flex; + margin-top: -2px; + flex-grow: 1; +} + +.edit-textbox { + flex-grow: 1; + font-family: 'PetMe64Web'; + font-size: 7pt; + margin-right: 5px; + border-color: rgb(0,0,0); + border-width: 1px; + border-style: solid; +} + +#edit-timelock { + display: flex; + align-items: center; + position: absolute; + margin-top: 4px; +} + +#edit-timelock:before { + position: absolute; + z-index: -1; + content : ""; + left : 8px; + top : 0; + height: 100%; + width : 5px; + border-left: 3px #666 solid; + border-top: 3px #666 solid; + border-bottom: 3px #666 solid; +} + +#edit-timelock.unlocked:before { + border-left: 3px #666 dashed; +} + +#edit-timelock > .hues-button { + /* because of the pseudo element this one can't be transparent */ + background-color: rgb(171,171,171) !important; + +} + +#edit-timelock:hover > .hues-button { + background-color: rgb(236,236,236) !important; +} + + +#edit-area { + flex-grow: 1; + /*height: initial !important;*/ + margin: 0 auto; + width: 100%; +} + +.edit-area-header { + padding-bottom: 8px; + margin-left: 15px; +} + +.beat-count { + font-size: 10px; +} + +.edit-area-right-header { + position: absolute; + right: 25px; +} + +.edit-box { + position: relative; + overflow-y : scroll; + background-color: white; + margin: auto 20px; +} + +.beatmap { + height: inherit; + width: inherit; + position: relative; +} + +.beat-hilight { + position: absolute; + color: rgba(127,127,127,0.5); + visibility: visible; + text-align: center; +} + +.beat-hilight.hidden { + visibility: hidden; +} + +#edit-build, #edit-loop { + height: auto; +} + +#edit-resize-handle-container { + width: 100%; + height: 20px; + cursor: row-resize; + overflow: hidden; +} + +#edit-resize-handle { + transform: scale(10.0, 1.0); + font-size: 20px; + text-align:center; + color: #999; +} + +#edit-resize-handle:hover { + color: #000; +} + +#edit-controls { + height: 25px !important; +} + +#edit-waveform { + width: 100%; + height: 50px; +} + +/* Make it as invisible as we can */ +#edit-copybox { + position : fixed; + top : 0; + left : 0; + width : 2em; + height : 2em; + padding : 0; + border : none; + outline : none; + background : transparent; +} \ No newline at end of file diff --git a/src/css/style.css b/src/css/style.css index 85f52aa..be1ac3a 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -154,6 +154,7 @@ h1, h2, h3 { #tabs { margin: -1px; padding-top: 22px; + display: flex; } input[type=radio] { @@ -161,48 +162,49 @@ input[type=radio] { } .tab-label{ + flex-grow: 1; cursor: pointer; - display: table-cell; padding: 10px; border: 2px solid black; - width: 1%; text-align: center; } -label.tab-label:hover { - background: rgba(255,255,255,0.3); +.tab-label.checked { + border-bottom: 0px; } -input.tab-input[type="radio"]:checked + label { - border-bottom: 0px; +l.tab-label:hover { + background: rgba(255,255,255,0.3); } .tab-content { display: none; } -#tab1:checked ~ #tab1-content, -#tab2:checked ~ #tab2-content, -#tab3:checked ~ #tab3-content, -#tab4:checked ~ #tab4-content { +#tab-resources:checked ~ #tab-resources-content, +#tab-editor:checked ~ #tab-editor-content, +#tab-options:checked ~ #tab-options-content, +#tab-info:checked ~ #tab-info-content { display: block; } #settingsHelper { + display: flex; + justify-content: center; + align-items: center; position: absolute; - left: 50%; - top: 50%; - width: 0; - height: 0; + margin-top: -15px; + width: 100%; + height: 100%; } #settingsWindow { position: relative; z-index: 9; - width: 640px; - height: 470px; - margin-left: -320px; - margin-top: -235px; + min-width: 640px; + min-height: 470px; + max-height: calc(100% - 50px); + margin: 10px; background: rgba(200,200,200, 0.7); border-color: black; @@ -212,6 +214,9 @@ input.tab-input[type="radio"]:checked + label { #huesSettings { padding: 5px; + width: 630px; + display: flex; + flex-wrap: wrap; } #closeButton { @@ -292,7 +297,7 @@ label.settings-label:hover { font-size: 10px; margin: 3px 2px; padding: 3px; - background: rgba(127,127,127, 0.5); + background-color: rgba(127,127,127, 0.5); border-color: rgb(0,0,0); border-width: 1px; border-style: solid; @@ -331,4 +336,22 @@ label.settings-label:hover { .hues-button.disabled:hover { background-color: rgba(127,127,127, 0.5); +} + +.hues-button.glow { + animation-name: glow; + animation-duration: 2s; + animation-iteration-count: infinite; +} + +@keyframes glow { + from { + background-color: rgba(127,127,127, 0.5); + } + 50% { + background-color: rgba(200,220,200, 0.5); + } + to { + background-color: rgba(127,127,127, 0.5); + } } \ No newline at end of file diff --git a/src/js/HuesCore.js b/src/js/HuesCore.js index c4546b6..3530069 100644 --- a/src/js/HuesCore.js +++ b/src/js/HuesCore.js @@ -290,6 +290,10 @@ HuesCore.prototype.animationLoop = function() { this.callEventListeners("frame"); }; +HuesCore.prototype.recalcBeatIndex = function() { + this.beatIndex = Math.floor(this.soundManager.clampedTime() / this.beatLength); +}; + HuesCore.prototype.getBeatIndex = function() { if(!this.soundManager.playing) { return 0; @@ -298,7 +302,7 @@ HuesCore.prototype.getBeatIndex = function() { } else { return this.beatIndex % this.currentSong.rhythm.length; } -} +}; HuesCore.prototype.getSafeBeatIndex = function() { var index = this.getBeatIndex(); @@ -329,19 +333,17 @@ HuesCore.prototype.setSongByName = function(name) { var songs = this.resourceManager.enabledSongs; for(var i = 0; i < songs.length; i++) { if(songs[i].title == name) { - this.setSong(i); - return; + return this.setSong(i); } } - this.setSong(0); // fallback + return this.setSong(0); // fallback }; /* To set songs via reference instead of index - used in HuesEditor */ HuesCore.prototype.setSongOject = function(song) { for(var i = 0; i < this.resourceManager.enabledSongs.length; i++) { if(this.resourceManager.enabledSongs[i] === song) { - this.setSong(i); - return; + return this.setSong(i); } } } @@ -375,7 +377,7 @@ HuesCore.prototype.setSong = function(index) { } } this.setInvert(false); - this.soundManager.playSong(this.currentSong, this.doBuildup) + return this.soundManager.playSong(this.currentSong, this.doBuildup) .then(() => { this.resetAudio(); this.fillBuildup(); @@ -393,20 +395,22 @@ HuesCore.prototype.updateBeatLength = function() { HuesCore.prototype.fillBuildup = function() { this.updateBeatLength(); - var buildBeats = Math.floor(this.soundManager.buildLength / this.beatLength); - if(buildBeats < 1) { - buildBeats = 1; - } - if (this.currentSong.buildupRhythm === null) { + if (!this.currentSong.buildupRhythm) { this.currentSong.buildupRhythm = ""; } - if (this.currentSong.buildupRhythm.length < buildBeats) { - console.log("Filling buildup beatmap"); - while (this.currentSong.buildupRhythm.length < buildBeats) { - this.currentSong.buildupRhythm = this.currentSong.buildupRhythm + "."; + if(this.currentSong.buildup) { + var buildBeats = Math.floor(this.soundManager.buildLength / this.beatLength); + if(buildBeats < 1) { + buildBeats = 1; + } + if (this.currentSong.buildupRhythm.length < buildBeats) { + console.log("Filling buildup beatmap"); + while (this.currentSong.buildupRhythm.length < buildBeats) { + this.currentSong.buildupRhythm = this.currentSong.buildupRhythm + "."; + } } + console.log("Buildup length:", buildBeats); } - console.log("Buildup length:", buildBeats); if(this.doBuildup) { this.beatIndex = -this.currentSong.buildupRhythm.length; } else { @@ -633,7 +637,7 @@ HuesCore.prototype.beater = function(beat) { break; } } - this.renderer.doColourFade(fadeLen * this.beatLength); + this.renderer.doColourFade((fadeLen * this.beatLength) / this.soundManager.playbackRate); this.randomColour(true); break; case 'I': diff --git a/src/js/HuesEditor.js b/src/js/HuesEditor.js new file mode 100644 index 0000000..db0cf5f --- /dev/null +++ b/src/js/HuesEditor.js @@ -0,0 +1,949 @@ +/* Copyright (c) William Toohey + * + * 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() { + 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 + var hilight = document.createElement("div"); + hilight.className = "beat-hilight"; + hilight.innerHTML = "█"; + this.root.appendChild(hilight); + this.hilightWidth = hilight.clientWidth; + this.hilightHeight = hilight.clientHeight; + 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 charWidth = editor._hilight.clientWidth; + var charsPerLine = Math.floor(editor._beatmap.clientWidth / charWidth); + // 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
"); + 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 => { + // First load, go fresh, get the core synced up + this.newSong(this.song); + + this.song[editor._sound] = buffer; + // 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(() => { + 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 resizer = (e) => { + var editTop = this.editArea.getBoundingClientRect().top; + this.buildEditSize = Math.floor(e.clientY - editTop); + this.resize(); + }; + + 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 = + '
\ + Click [LOAD RHYTHM] to load a loop! LAME encoded MP3s work best.
\ + (LAME is important for seamless MP3 loops)
\ +
\ + [DOUBLE] doubles the selected map length by padding it with "."s.
\ + [HALVE] shortens the map length by removing every other character.
\ + [<< START] starts the song from the beginning.
\ + [< LOOP] starts the song from the start of the loop.
\ +
\ + You can also add a buildup with [LOAD BUILDUP], or remove it
\ + with [REMOVE].
\ +
\ + [NEW SONG] adds a completely empty song for you to edit, and
\ + [EDIT CURRENT SONG] takes the current playing song to the editor.
\ +
\ + [COPY/SAVE XML] allow for storing the rhythms and easy
\ + 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); + + 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.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); + //
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(); + //
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 = " \n"; + result += " " + this.song.title + "\n"; + if(this.song.source) { + result += " " + this.song.source + "\n"; + } + result += " " + this.song.rhythm + "\n"; + if(this.song.buildup) { + result += " " + this.song.buildupName + "\n"; + result += " " + this.song.buildupRhythm + "\n"; + } + result += " \n"; + return result; +} + +HuesEditor.prototype.saveXML = function() { + var xml = this.generateXML(); + if(!xml) { + return; + } + var result = "\n"; + result += xml; + result += "\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"); + } + +} + + function drawWaveform(buffer) { + var ca = document.querySelector("#waveform"); + var c = ca.getContext("2d"); + + var samples = buffer.getChannelData(0); + var width = (buffer.duration * secondWidth) | 0; + + ca.width = width; + ca.height = 128; + + c.strokeStyle = "black"; + + var samplesInColumn = (samples.length / width) | 0; + + // console.log(samplesInColumn) + + var t, b; + for (var i = 0; i < width; i++) { + var pmin = 999; + var pmax = 0; + + var nmin = 0; + var nmax = -999; + for (var j = 0; j < samplesInColumn; j++) { + var s = samples[i*samplesInColumn+j] + // console.log(s) + if (s > 0 && s < pmin) { pmin = s;} + else if (s > 0 && s > pmax) { pmax = s;} + + else if (s < 0 && s < nmin) { nmin = s;} + else if (s < 0 && s > nmax) { nmax = s;} + }; + + // console.log(pmax, pmin, nmax, nmin) + var ch = ca.height; + + // t = (pmax) * ch/2 + // b = (2+nmin) * ch/2 + + t = (1-pmax) * ch/2 + b = ch - (1+nmin) * ch/2 + + + + c.strokeStyle = "black"; + c.beginPath() + c.moveTo(i, t) + c.lineTo(i, b) + c.stroke(); + + // t = (1-pmax) * ch/2 + // b = ch - (1+nmin) * ch/2 + // c.strokeStyle = "green"; + // c.beginPath() + // c.moveTo(i, t) + // c.lineTo(i, b) + // c.stroke(); + // pixel(c, i, t, [255,0,0,255]) + // pixel(c, i, b, [0,0,255,255]) + // console.log(pmin, nmax) + }; + + } + +window.HuesEditor = HuesEditor; + +})(window, document); \ No newline at end of file diff --git a/src/js/HuesSettings.js b/src/js/HuesSettings.js index fdca3af..df45f3f 100644 --- a/src/js/HuesSettings.js +++ b/src/js/HuesSettings.js @@ -238,6 +238,18 @@ function HuesSettings(defaults) { // because we still care about the main window document.getElementById("closeButton").onclick = this.hide.bind(this); + // we also care about tabs looking nice. + var tabs = document.getElementsByClassName("tab-label"); + for(var i = 0; i < tabs.length; i++) { + var check = document.getElementById(tabs[i].htmlFor); + check._label = tabs[i]; + check.addEventListener("change", function() { // Uses 'this' of check! + for(var i = 0; i < tabs.length; i++) { + tabs[i].className = "tab-label"; + } + this._label.className = "tab-label checked"; + }); + } if(!this.defaults.noUI) { this.initUI(); } @@ -251,7 +263,8 @@ HuesSettings.prototype.connectCore = function(core) { HuesSettings.prototype.show = function() { if(this.core) this.core.hideLists(); - this.window.style.display = "block"; + this.window.style.display = "-webkit-flex"; + this.window.style.display = "flex"; }; HuesSettings.prototype.hide = function() { @@ -260,32 +273,30 @@ HuesSettings.prototype.hide = function() { HuesSettings.prototype.toggle = function() { if(this.window.style.display == "none") { - this.window.style.display = "block"; - if(this.core) - this.core.hideLists(); + this.show(); } else { - this.window.style.display = "none"; + this.hide(); } }; HuesSettings.prototype.showRespacks = function() { this.show(); - document.getElementById("tab1").checked = true; + document.getElementById("tab-resources").click(); }; HuesSettings.prototype.showEditor = function() { this.show(); - document.getElementById("tab2").checked = true; + document.getElementById("tab-editor").click(); }; HuesSettings.prototype.showOptions = function() { this.show(); - document.getElementById("tab3").checked = true; + document.getElementById("tab-options").click(); }; HuesSettings.prototype.showInfo = function() { this.show(); - document.getElementById("tab4").checked = true; + document.getElementById("tab-info").click(); }; HuesSettings.prototype.initUI = function() { diff --git a/src/js/ResourcePack.js b/src/js/ResourcePack.js index ecd3e48..3602a85 100644 --- a/src/js/ResourcePack.js +++ b/src/js/ResourcePack.js @@ -421,17 +421,17 @@ Respack.prototype.parseSongFile = function(text) { if(song.buildupLength) { debug("Using custom BU length:", song.buildupLength); } - song.buildup = el.getTag("buildup"); - if(song.buildup) { - debug(" Finding a buildup '" + song.buildup + "' for ", song.name); - var build = this.getSong(song.buildup); + song.buildupName = el.getTag("buildup"); + if(song.buildupName) { + debug(" Finding a buildup '" + song.buildupName + "' for ", song.name); + var build = this.getSong(song.buildupName); if(build) { song.buildup = build.sound; song.buildupPlayed = false; // get rid of the junk this.songs.splice(this.songs.indexOf(build), 1); } else { - debug(" WARNING!", "Didn't find a buildup '" + song.buildup + "'!"); + debug(" WARNING!", "Didn't find a buildup '" + song.buildupName + "'!"); } } diff --git a/src/js/SoundManager.js b/src/js/SoundManager.js index 95f5899..fb45301 100644 --- a/src/js/SoundManager.js +++ b/src/js/SoundManager.js @@ -25,6 +25,7 @@ function SoundManager(core) { this.core = core; this.playing = false; + this.playbackRate = 1; this.song = null; this.initPromise = null; @@ -130,9 +131,10 @@ SoundManager.prototype.init = function() { return this.initPromise; } -SoundManager.prototype.playSong = function(song, playBuild) { +SoundManager.prototype.playSong = function(song, playBuild, forcePlay) { var p = Promise.resolve(); - if(this.song == song) { + // Editor forces play on audio updates + if(this.song == song && !forcePlay) { return p; } this.stop(); @@ -173,8 +175,6 @@ SoundManager.prototype.playSong = function(song, playBuild) { } return this.context.resume(); - }).then(() => { - this.playing = true; }).catch(error => { // Just to ignore it if the song was invalid // Log it in case it's something weird @@ -202,8 +202,20 @@ SoundManager.prototype.stop = function() { } }; +SoundManager.prototype.setRate = function(rate) { + // Double speed is more than enough. Famous last words? + rate = Math.max(Math.min(rate, 2), 0.25); + + var time = this.clampedTime(); + this.playbackRate = rate; + this.seek(time); +} + SoundManager.prototype.seek = function(time) { - console.log("Seeking to " + time); + if(!this.song) { + return; + } + //console.log("Seeking to " + time); // Clamp the blighter time = Math.min(Math.max(time, -this.buildLength), this.loopLength); @@ -211,6 +223,7 @@ SoundManager.prototype.seek = function(time) { this.loopSource = this.context.createBufferSource(); this.loopSource.buffer = this.loop; + this.loopSource.playbackRate.value = this.playbackRate; this.loopSource.loop = true; this.loopSource.loopStart = 0; this.loopSource.loopEnd = this.loopLength; @@ -219,14 +232,17 @@ SoundManager.prototype.seek = function(time) { if(time < 0) { this.buildSource = this.context.createBufferSource(); this.buildSource.buffer = this.buildup; + this.buildSource.playbackRate.value = this.playbackRate; this.buildSource.connect(this.gainNode); this.buildSource.start(0, this.buildLength + time); - this.loopSource.start(this.context.currentTime - time); + this.loopSource.start(this.context.currentTime - (time / this.playbackRate)); } else { this.loopSource.start(0, time); } - this.startTime = this.context.currentTime - time; + this.startTime = this.context.currentTime - (time / this.playbackRate); + this.playing = true; + this.core.recalcBeatIndex(); } // In seconds, relative to the loop start @@ -234,18 +250,24 @@ SoundManager.prototype.currentTime = function() { if(!this.playing) { return 0; } - return this.context.currentTime - this.startTime; + return (this.context.currentTime - this.startTime) * this.playbackRate; }; -SoundManager.prototype.displayableTime = function() { - if(!this.playing) { - return 0; - } +SoundManager.prototype.clampedTime = function() { var time = this.currentTime(); + + if(time > 0) { + time %= this.loopLength; + } + return time; +} + +SoundManager.prototype.displayableTime = function() { + var time = this.clampedTime(); if(time < 0) { return 0; } else { - return time % this.loopLength; + return time; } };