diff --git a/package.json b/package.json index 6b76c46..91fe784 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "homepage": "https://github.com/mon/0x40-web#readme", "devDependencies": { - "babel-preset-es2015": "^6.6.0", + "babel-preset-es2015": "^6.9.0", "del": "^2.2.0", "gulp": "^3.9.1", "gulp-autoprefixer": "^3.1.0", @@ -33,6 +33,6 @@ "gulp-plumber": "^1.1.0", "gulp-sourcemaps": "^1.6.0", "gulp-uglify": "^1.5.3", - "jshint": "^2.9.1" + "jshint": "^2.9.2" } } diff --git a/src/js/HuesCanvas.js b/src/js/HuesCanvas.js index 4cbde4f..7d549ef 100644 --- a/src/js/HuesCanvas.js +++ b/src/js/HuesCanvas.js @@ -25,149 +25,186 @@ /* Takes root element to attach to, and an audio context element for getting the current time with reasonable accuracy */ -function HuesCanvas(root, audioContext, core) { - this.audio = audioContext; - core.addEventListener("newimage", this.setImage.bind(this)); - core.addEventListener("newcolour", this.setColour.bind(this)); - core.addEventListener("beat", this.beat.bind(this)); - core.addEventListener("invert", this.setInvert.bind(this)); - core.addEventListener("settingsupdated", this.settingsUpdated.bind(this)); - core.addEventListener("frame", this.animationLoop.bind(this)); - this.core = core; - - this.needsRedraw = false; - this.colour = 0xFFFFFF; - this.image = null; - this.smartAlign = true; // avoid string comparisons every frame - - this.animTimeout = null; - this.animFrame = null; - this.lastBeat = 0; - - // set later - this.blurDecay = null; - this.blurAmount = null; - this.blurIterations = null; - this.blurDelta = null; - this.blurAlpha = null; - // dynamic - this.blurStart = 0; - this.blurDistance = 0; - this.xBlur = false; - this.yBlur = false; - - // trippy mode - this.trippyStart = [0, 0]; // x, y - this.trippyRadii = [0, 0]; // x, y - // force trippy mode - this.trippyOn = false; - this.trippyRadius = 0; - - this.blackout = false; - this.blackoutColour = "#000"; // for the whiteout case we must store this - this.blackoutTimeout = null; - - this.invert = false; - - this.colourFade = false; - this.colourFadeStart=0; - this.colourFadeLength=0; - this.oldColour=0xFFFFFF; - this.newColour=0xFFFFFF; - - this.blendMode = "hard-light"; - // Chosen because they look decent - this.setBlurAmount("medium"); - this.setBlurQuality("high"); - this.setBlurDecay("fast"); - - this.canvas = document.createElement('canvas'); - this.context = this.canvas.getContext("2d"); - this.canvas.width = 1280; - this.canvas.height = 720; - this.canvas.className = "hues-canvas"; - root.appendChild(this.canvas); - - this.offCanvas = document.createElement('canvas'); - this.offContext = this.offCanvas.getContext('2d'); - - window.addEventListener('resize', this.resize.bind(this)); - this.resize(); -} +class HuesCanvas { + constructor(root, audioContext, core) { + this.audio = audioContext; + core.addEventListener("newimage", this.setImage.bind(this)); + core.addEventListener("newcolour", this.setColour.bind(this)); + core.addEventListener("beat", this.beat.bind(this)); + core.addEventListener("invert", this.setInvert.bind(this)); + core.addEventListener("settingsupdated", this.settingsUpdated.bind(this)); + core.addEventListener("frame", this.animationLoop.bind(this)); + this.core = core; -HuesCanvas.prototype.setInvert = function(invert) { - this.invert = invert; - this.needsRedraw = true; -}; - -HuesCanvas.prototype.settingsUpdated = function() { - this.setSmartAlign(localStorage["smartAlign"]); - this.setBlurAmount(localStorage["blurAmount"]); - this.setBlurDecay(localStorage["blurDecay"]); - this.setBlurQuality(localStorage["blurQuality"]); - this.trippyOn = localStorage["trippyMode"] == "on"; -}; - -HuesCanvas.prototype.resize = function() { - // height is max 720px, we expand width to suit - let height = this.core.root.clientHeight; - let ratio = this.core.root.clientWidth / height; - this.canvas.height = Math.min(height, 720); - this.canvas.width = Math.ceil(this.canvas.height * ratio); - this.offCanvas.height = this.canvas.height; - this.offCanvas.width = this.canvas.width; - this.trippyRadius = Math.max(this.canvas.width, this.canvas.height) / 2; - this.needsRedraw = true; -}; - -HuesCanvas.prototype.redraw = function() { - let offset; // for centering/right/left align - let bOpacity; - let width = this.canvas.width; - let height = this.canvas.height; - - let cTime = this.audio.currentTime; - // white BG for the hard light filter - this.context.globalAlpha = 1; - this.context.globalCompositeOperation = "source-over"; - if(this.blackout) { - // original is 3 frames at 30fps, this is close - bOpacity = (cTime - this.blackoutStart)*10; - if(bOpacity > 1) { // optimise the draw - this.context.fillStyle = this.blackoutColour; + this.needsRedraw = false; + this.colour = 0xFFFFFF; + this.image = null; + this.smartAlign = true; // avoid string comparisons every frame + + this.animTimeout = null; + this.animFrame = null; + this.lastBeat = 0; + + // set later + this.blurDecay = null; + this.blurAmount = null; + this.blurIterations = null; + this.blurDelta = null; + this.blurAlpha = null; + // dynamic + this.blurStart = 0; + this.blurDistance = 0; + this.xBlur = false; + this.yBlur = false; + + // trippy mode + this.trippyStart = [0, 0]; // x, y + this.trippyRadii = [0, 0]; // x, y + // force trippy mode + this.trippyOn = false; + this.trippyRadius = 0; + + this.blackout = false; + this.blackoutColour = "#000"; // for the whiteout case we must store this + this.blackoutTimeout = null; + + this.invert = false; + + this.colourFade = false; + this.colourFadeStart=0; + this.colourFadeLength=0; + this.oldColour=0xFFFFFF; + this.newColour=0xFFFFFF; + + this.blendMode = "hard-light"; + // Chosen because they look decent + this.setBlurAmount("medium"); + this.setBlurQuality("high"); + this.setBlurDecay("fast"); + + this.canvas = document.createElement('canvas'); + this.context = this.canvas.getContext("2d"); + this.canvas.width = 1280; + this.canvas.height = 720; + this.canvas.className = "hues-canvas"; + root.appendChild(this.canvas); + + this.offCanvas = document.createElement('canvas'); + this.offContext = this.offCanvas.getContext('2d'); + + window.addEventListener('resize', this.resize.bind(this)); + this.resize(); + } + + setInvert(invert) { + this.invert = invert; + this.needsRedraw = true; + } + + settingsUpdated() { + this.setSmartAlign(localStorage["smartAlign"]); + this.setBlurAmount(localStorage["blurAmount"]); + this.setBlurDecay(localStorage["blurDecay"]); + this.setBlurQuality(localStorage["blurQuality"]); + this.trippyOn = localStorage["trippyMode"] == "on"; + } + + resize() { + // height is max 720px, we expand width to suit + let height = this.core.root.clientHeight; + let ratio = this.core.root.clientWidth / height; + this.canvas.height = Math.min(height, 720); + this.canvas.width = Math.ceil(this.canvas.height * ratio); + this.offCanvas.height = this.canvas.height; + this.offCanvas.width = this.canvas.width; + this.trippyRadius = Math.max(this.canvas.width, this.canvas.height) / 2; + this.needsRedraw = true; + } + + redraw() { + let offset; // for centering/right/left align + let bOpacity; + let width = this.canvas.width; + let height = this.canvas.height; + + let cTime = this.audio.currentTime; + // white BG for the hard light filter + this.context.globalAlpha = 1; + this.context.globalCompositeOperation = "source-over"; + if(this.blackout) { + // original is 3 frames at 30fps, this is close + bOpacity = (cTime - this.blackoutStart)*10; + if(bOpacity > 1) { // optimise the draw + this.context.fillStyle = this.blackoutColour; + this.context.fillRect(0,0,width,height); + this.needsRedraw = false; + this.drawInvert(); + return; + } + } else { + this.context.fillStyle = "#FFF"; this.context.fillRect(0,0,width,height); - this.needsRedraw = false; - this.drawInvert(); - return; } - } else { - this.context.fillStyle = "#FFF"; - this.context.fillRect(0,0,width,height); - } - if(this.image && (this.image.bitmap || this.image.bitmaps)) { - let bitmap = this.image.animated ? - this.image.bitmaps[this.animFrame] : this.image.bitmap; - let drawHeight = bitmap.height * (height / bitmap.height); - let drawWidth = (bitmap.width / bitmap.height) * drawHeight; - if(this.smartAlign) { - switch(this.image.align) { - case "left": - offset = 0; - break; - case "right": - offset = width - drawWidth; - break; - default: - offset = width/2 - drawWidth/2; - break; + if(this.image && (this.image.bitmap || this.image.bitmaps)) { + let bitmap = this.image.animated ? + this.image.bitmaps[this.animFrame] : this.image.bitmap; + let drawHeight = bitmap.height * (height / bitmap.height); + let drawWidth = (bitmap.width / bitmap.height) * drawHeight; + if(this.smartAlign) { + switch(this.image.align) { + case "left": + offset = 0; + break; + case "right": + offset = width - drawWidth; + break; + default: + offset = width/2 - drawWidth/2; + break; + } + } else { + offset = width/2 - drawWidth/2; } + if(this.xBlur || this.yBlur) { + this.drawBlur(bitmap, offset, drawWidth, drawHeight); + }else { + this.context.globalAlpha = 1; + this.context.drawImage(bitmap, offset, 0, drawWidth, drawHeight); + } + } + + if(this.trippyStart[0] || this.trippyStart[1]) { + this.drawTrippy(width, height); } else { - offset = width/2 - drawWidth/2; + this.offContext.fillStyle = this.intToHex(this.colour); + this.offContext.fillRect(0,0,width,height); } - if(this.xBlur || this.yBlur) { - this.context.globalAlpha = this.blurAlpha; + this.context.globalAlpha = 0.7; + this.context.globalCompositeOperation = this.blendMode; + this.context.drawImage(this.offCanvas, 0, 0); + if(this.blackout) { + this.context.globalAlpha = bOpacity; + this.context.fillStyle = this.blackoutColour; + this.context.fillRect(0,0,width,height); + this.needsRedraw = true; + } else { + this.needsRedraw = false; } + this.drawInvert(); + } + + drawInvert() { + if(this.invert) { + this.context.globalAlpha = 1; + this.context.globalCompositeOperation = "difference"; + this.context.fillStyle = "#FFF"; + this.context.fillRect(0,0,this.canvas.width,this.canvas.height); + } + } + + drawBlur(bitmap, offset, drawWidth, drawHeight) { + this.context.globalAlpha = this.blurAlpha; if(this.xBlur) { if(this.blurIterations < 0) { this.context.globalAlpha = 1; @@ -188,13 +225,11 @@ HuesCanvas.prototype.redraw = function() { this.context.drawImage(bitmap, offset, Math.floor(this.blurDistance * i), drawWidth, drawHeight); } } - } else { - this.context.globalAlpha = 1; - this.context.drawImage(bitmap, offset, 0, drawWidth, drawHeight); } } - - if(this.trippyStart[0] || this.trippyStart[1]) { + + // draws the correct trippy colour circles onto the offscreen canvas + drawTrippy(width, height) { // x blur moves inwards from the corners, y comes out // So the base colour is inverted for y, normal for x // Thus if the y start is more recent, we invert @@ -222,289 +257,265 @@ HuesCanvas.prototype.redraw = function() { this.offContext.closePath(); invert = !invert; } - } else { - this.offContext.fillStyle = this.intToHex(this.colour); - this.offContext.fillRect(0,0,width,height); } - this.context.globalAlpha = 0.7; - this.context.globalCompositeOperation = this.blendMode; - this.context.drawImage(this.offCanvas, 0, 0); - if(this.blackout) { - this.context.globalAlpha = bOpacity; - this.context.fillStyle = this.blackoutColour; - this.context.fillRect(0,0,width,height); - this.needsRedraw = true; - } else { - this.needsRedraw = false; - } - this.drawInvert(); -}; -HuesCanvas.prototype.drawInvert = function() { - if(this.invert) { - this.context.globalAlpha = 1; - this.context.globalCompositeOperation = "difference"; - this.context.fillStyle = "#FFF"; - this.context.fillRect(0,0,this.canvas.width,this.canvas.height); + /* Second fastest method from + http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript + It stil does millions of ops per second, and isn't ugly like the integer if/else */ + intToHex(num) { + return '#' + ("00000"+num.toString(16)).slice(-6); } -}; - -/* Second fastest method from - http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript - It stil does millions of ops per second, and isn't ugly like the integer if/else */ -HuesCanvas.prototype.intToHex = function(num) { - return '#' + ("00000"+num.toString(16)).slice(-6); -}; - -HuesCanvas.prototype.animationLoop = function() { - if (this.colourFade) { - let delta = this.audio.currentTime - this.colourFadeStart; - let fadeVal = delta / this.colourFadeLength; - if (fadeVal >= 1) { - this.stopFade(); - this.colour = this.newColour; - } else { - this.mixColours(fadeVal); + + animationLoop() { + if (this.colourFade) { + let delta = this.audio.currentTime - this.colourFadeStart; + let fadeVal = delta / this.colourFadeLength; + if (fadeVal >= 1) { + this.stopFade(); + this.colour = this.newColour; + } else { + this.mixColours(fadeVal); + } + this.needsRedraw = true; } - this.needsRedraw = true; - } - if(this.blackoutTimeout && this.audio.currentTime > this.blackoutTimeout) { - this.clearBlackout(); - } - if(this.image && this.image.animated){ - if(this.image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) { - let a = this.animFrame; - this.syncAnim(); - if(this.animFrame != a) { + if(this.blackoutTimeout && this.audio.currentTime > this.blackoutTimeout) { + this.clearBlackout(); + } + if(this.image && this.image.animated){ + if(this.image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) { + let a = this.animFrame; + this.syncAnim(); + if(this.animFrame != a) { + this.needsRedraw = true; + // If you change to a non-synced song, this needs to be reset + this.animTimeout = this.audio.currentTime; + } + } else if(this.animTimeout < this.audio.currentTime) { + this.animFrame++; + this.animFrame %= this.image.frameDurations.length; + // Don't rebase to current time otherwise we may lag + this.animTimeout += this.image.frameDurations[this.animFrame]/1000; this.needsRedraw = true; - // If you change to a non-synced song, this needs to be reset - this.animTimeout = this.audio.currentTime; } - } else if(this.animTimeout < this.audio.currentTime) { - this.animFrame++; - this.animFrame %= this.image.frameDurations.length; - // Don't rebase to current time otherwise we may lag - this.animTimeout += this.image.frameDurations[this.animFrame]/1000; + } + if(this.blurStart) { + // flash offsets blur gen by a frame + let delta = this.audio.currentTime - this.blurStart + (1/30); + this.blurDistance = this.blurAmount * Math.exp(-this.blurDecay * delta); + + // Update UI + let dist = this.blurDistance / this.blurAmount; + if(this.xBlur) + this.core.blurUpdated(dist, 0); + else + this.core.blurUpdated(0, dist); + } + if(this.trippyStart[0] || this.trippyStart[1]) { + for(let i = 0; i < 2; i++) { + this.trippyRadii[i] = Math.floor((this.audio.currentTime - this.trippyStart[i]) * this.trippyRadius) * 2; + if(this.trippyRadii[i] > this.trippyRadius) { + this.trippyStart[i] = 0; + this.trippyRadii[i] = 0; + continue; + } + // x comes from outside the window + if(i % 2 === 0) { + this.trippyRadii[i] = this.trippyRadius - this.trippyRadii[i]; + } + } this.needsRedraw = true; } - } - if(this.blurStart) { - // flash offsets blur gen by a frame - let delta = this.audio.currentTime - this.blurStart + (1/30); - this.blurDistance = this.blurAmount * Math.exp(-this.blurDecay * delta); - // Update UI - let dist = this.blurDistance / this.blurAmount; - if(this.xBlur) - this.core.blurUpdated(dist, 0); - else - this.core.blurUpdated(0, dist); + if(this.blurStart && this.blurDistance < 1) { + this.core.blurUpdated(0, 0); + this.blurDistance = 0; + this.blurStart = 0; + this.xBlur = this.yBlur = false; + this.redraw(); + } else if(this.blurStart) { + this.redraw(); + } else if(this.needsRedraw){ + this.redraw(); + } } - if(this.trippyStart[0] || this.trippyStart[1]) { - for(let i = 0; i < 2; i++) { - this.trippyRadii[i] = Math.floor((this.audio.currentTime - this.trippyStart[i]) * this.trippyRadius) * 2; - if(this.trippyRadii[i] > this.trippyRadius) { - this.trippyStart[i] = 0; - this.trippyRadii[i] = 0; - continue; - } - // x comes from outside the window - if(i % 2 === 0) { - this.trippyRadii[i] = this.trippyRadius - this.trippyRadii[i]; - } + + setImage(image) { + if(this.image == image) { + return; } this.needsRedraw = true; + this.image = image; + // Null images don't need anything interesting done to them + if(!image || (!image.bitmap && !image.bitmaps)) { + return; + } + if(image.animated) { + this.animBeat = null; + this.animFrame = 0; + this.animTimeout = this.audio.currentTime + image.frameDurations[0]/1000; + if(image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) { + this.syncAnim(); + } + } } - - if(this.blurStart && this.blurDistance < 1) { - this.core.blurUpdated(0, 0); - this.blurDistance = 0; - this.blurStart = 0; - this.xBlur = this.yBlur = false; - this.redraw(); - } else if(this.blurStart) { - this.redraw(); - } else if(this.needsRedraw){ - this.redraw(); + + beat() { + this.lastBeat = this.audio.currentTime; + } + + syncAnim() { + let song = this.core.currentSong; + if(!song) { // fallback to default + return; + } + let index = this.core.beatIndex; + // When animation has more frames than song has beats, or part thereof + if(this.lastBeat && this.core.getBeatLength()) { + let interp = (this.audio.currentTime - this.lastBeat) / this.core.getBeatLength(); + index += Math.min(interp, 1); + } + // This loops A-OK because the core's beatIndex never rolls over for a new loop + let beatLoc = (index / song.charsPerBeat) % this.image.beatsPerAnim; + + let aLen = this.image.bitmaps.length; + this.animFrame = Math.floor(aLen * (beatLoc / this.image.beatsPerAnim)); + if(this.image.syncOffset) { + this.animFrame += this.image.syncOffset; + } + // Because negative mods are different in JS + this.animFrame = ((this.animFrame % aLen) + aLen) % aLen; } -}; -HuesCanvas.prototype.setImage = function(image) { - if(this.image == image) { - return; + setColour(colour, isFade) { + if(colour.c == this.colour) { + return; + } + if(isFade) { + this.newColour = colour.c; + } else { + this.stopFade(); + this.colour = colour.c; + } + this.needsRedraw = true; } - this.needsRedraw = true; - this.image = image; - // Null images don't need anything interesting done to them - if(!image || (!image.bitmap && !image.bitmaps)) { - return; + + doBlackout(whiteout) { + if (typeof(whiteout)==='undefined') whiteout = false; + if(whiteout) { + this.blackoutColour = "#FFF"; + } else { + this.blackoutColour = "#000"; + } + this.blackoutTimeout = 0; // indefinite + // Don't restart the blackout animation if we're already blacked out + if(!this.blackout) { + this.blackoutStart = this.audio.currentTime; + } + this.blackout = true; + this.needsRedraw = true; + if(localStorage["blackoutUI"] == "on") { + this.core.userInterface.hide(); + } } - if(image.animated) { - this.animBeat = null; - this.animFrame = 0; - this.animTimeout = this.audio.currentTime + image.frameDurations[0]/1000; - if(image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) { - this.syncAnim(); + + // for song changes + clearBlackout() { + this.blackout = false; + this.blackoutTimeout = 0; + this.needsRedraw = true; + if(localStorage["blackoutUI"] == "on") { + this.core.userInterface.show(); } } -}; -HuesCanvas.prototype.beat = function() { - this.lastBeat = this.audio.currentTime; -}; + doShortBlackout(beatTime) { + this.doBlackout(); + this.blackoutTimeout = this.audio.currentTime + beatTime / 1.7; + // looks better if we go right to black + this.blackoutStart = 0; + } + + doColourFade(length) { + this.colourFade = true; + this.colourFadeLength = length; + this.colourFadeStart = this.audio.currentTime; + this.oldColour = this.colour; + } -HuesCanvas.prototype.syncAnim = function() { - let song = this.core.currentSong; - if(!song) { // fallback to default - return; + stopFade() { + this.colourFade = false; + this.colourFadeStart = 0; + this.colourFadeLength = 0; } - let index = this.core.beatIndex; - // When animation has more frames than song has beats, or part thereof - if(this.lastBeat && this.core.getBeatLength()) { - let interp = (this.audio.currentTime - this.lastBeat) / this.core.getBeatLength(); - index += Math.min(interp, 1); + + mixColours(percent) { + percent = Math.min(1, percent); + let oldR = this.oldColour >> 16 & 0xFF; + let oldG = this.oldColour >> 8 & 0xFF; + let oldB = this.oldColour & 0xFF; + let newR = this.newColour >> 16 & 0xFF; + let newG = this.newColour >> 8 & 0xFF; + let newB = this.newColour & 0xFF; + let mixR = oldR * (1 - percent) + newR * percent; + let mixG = oldG * (1 - percent) + newG * percent; + let mixB = oldB * (1 - percent) + newB * percent; + this.colour = mixR << 16 | mixG << 8 | mixB; + } + + doXBlur() { + this.blurStart = this.audio.currentTime; + if(this.trippyOn) + this.trippyStart[0] = this.blurStart; + this.blurDistance = this.blurAmount; + this.xBlur = true; + this.yBlur = false; + this.needsRedraw = true; } - // This loops A-OK because the core's beatIndex never rolls over for a new loop - let beatLoc = (index / song.charsPerBeat) % this.image.beatsPerAnim; - let aLen = this.image.bitmaps.length; - this.animFrame = Math.floor(aLen * (beatLoc / this.image.beatsPerAnim)); - if(this.image.syncOffset) { - this.animFrame += this.image.syncOffset; + doYBlur() { + this.blurStart = this.audio.currentTime; + if(this.trippyOn) + this.trippyStart[1] = this.blurStart; + this.blurDistance = this.blurAmount; + this.xBlur = false; + this.yBlur = true; + this.needsRedraw = true; } - // Because negative mods are different in JS - this.animFrame = ((this.animFrame % aLen) + aLen) % aLen; -}; -HuesCanvas.prototype.setColour = function(colour, isFade) { - if(colour.c == this.colour) { - return; + doTrippyX() { + let saveTrippy = this.trippyOn; + // force trippy + this.trippyOn = true; + this.doXBlur(); + this.trippyOn = saveTrippy; } - if(isFade) { - this.newColour = colour.c; - } else { - this.stopFade(); - this.colour = colour.c; + + doTrippyY() { + let saveTrippy = this.trippyOn; + // force trippy + this.trippyOn = true; + this.doYBlur(); + this.trippyOn = saveTrippy; } - this.needsRedraw = true; -}; - -HuesCanvas.prototype.doBlackout = function(whiteout) { - if (typeof(whiteout)==='undefined') whiteout = false; - if(whiteout) { - this.blackoutColour = "#FFF"; - } else { - this.blackoutColour = "#000"; + + setBlurDecay(decay) { + this.blurDecay = {"slow" : 7.8, "medium" : 14.1, "fast" : 20.8, "faster!" : 28.7}[decay]; } - this.blackoutTimeout = 0; // indefinite - // Don't restart the blackout animation if we're already blacked out - if(!this.blackout) { - this.blackoutStart = this.audio.currentTime; + + setBlurQuality(quality) { + this.blurIterations = {"low" : -1, "medium" : 11, "high" : 19, "extreme" : 35}[quality]; + this.blurDelta = 1 / (this.blurIterations/2); + this.blurAlpha = 1 / (this.blurIterations/2); } - this.blackout = true; - this.needsRedraw = true; - if(localStorage["blackoutUI"] == "on") { - this.core.userInterface.hide(); + + setBlurAmount(amount) { + this.blurAmount = {"low" : 48, "medium" : 96, "high" : 384}[amount]; } -}; - -// for song changes -HuesCanvas.prototype.clearBlackout = function() { - this.blackout = false; - this.blackoutTimeout = 0; - this.needsRedraw = true; - if(localStorage["blackoutUI"] == "on") { - this.core.userInterface.show(); + + setSmartAlign(align) { + this.smartAlign = align == "on"; } -}; - -HuesCanvas.prototype.doShortBlackout = function(beatTime) { - this.doBlackout(); - this.blackoutTimeout = this.audio.currentTime + beatTime / 1.7; - // looks better if we go right to black - this.blackoutStart = 0; -}; - -HuesCanvas.prototype.doColourFade = function(length) { - this.colourFade = true; - this.colourFadeLength = length; - this.colourFadeStart = this.audio.currentTime; - this.oldColour = this.colour; -}; - -HuesCanvas.prototype.stopFade = function() { - this.colourFade = false; - this.colourFadeStart = 0; - this.colourFadeLength = 0; -}; - -HuesCanvas.prototype.mixColours = function(percent) { - percent = Math.min(1, percent); - let oldR = this.oldColour >> 16 & 0xFF; - let oldG = this.oldColour >> 8 & 0xFF; - let oldB = this.oldColour & 0xFF; - let newR = this.newColour >> 16 & 0xFF; - let newG = this.newColour >> 8 & 0xFF; - let newB = this.newColour & 0xFF; - let mixR = oldR * (1 - percent) + newR * percent; - let mixG = oldG * (1 - percent) + newG * percent; - let mixB = oldB * (1 - percent) + newB * percent; - this.colour = mixR << 16 | mixG << 8 | mixB; -}; - -HuesCanvas.prototype.doXBlur = function() { - this.blurStart = this.audio.currentTime; - if(this.trippyOn) - this.trippyStart[0] = this.blurStart; - this.blurDistance = this.blurAmount; - this.xBlur = true; - this.yBlur = false; - this.needsRedraw = true; -}; - -HuesCanvas.prototype.doYBlur = function() { - this.blurStart = this.audio.currentTime; - if(this.trippyOn) - this.trippyStart[1] = this.blurStart; - this.blurDistance = this.blurAmount; - this.xBlur = false; - this.yBlur = true; - this.needsRedraw = true; -}; - -HuesCanvas.prototype.doTrippyX = function() { - let saveTrippy = this.trippyOn; - // force trippy - this.trippyOn = true; - this.doXBlur(); - this.trippyOn = saveTrippy; -}; - -HuesCanvas.prototype.doTrippyY = function() { - let saveTrippy = this.trippyOn; - // force trippy - this.trippyOn = true; - this.doYBlur(); - this.trippyOn = saveTrippy; -}; - -HuesCanvas.prototype.setBlurDecay = function(decay) { - this.blurDecay = {"slow" : 7.8, "medium" : 14.1, "fast" : 20.8, "faster!" : 28.7}[decay]; -}; - -HuesCanvas.prototype.setBlurQuality = function(quality) { - this.blurIterations = {"low" : -1, "medium" : 11, "high" : 19, "extreme" : 35}[quality]; - this.blurDelta = 1 / (this.blurIterations/2); - this.blurAlpha = 1 / (this.blurIterations/2); -}; - -HuesCanvas.prototype.setBlurAmount = function(amount) { - this.blurAmount = {"low" : 48, "medium" : 96, "high" : 384}[amount]; -}; - -HuesCanvas.prototype.setSmartAlign = function(align) { - this.smartAlign = align == "on"; -}; +} window.HuesCanvas = HuesCanvas; diff --git a/src/js/HuesCore.js b/src/js/HuesCore.js index daf8a61..e7192f0 100644 --- a/src/js/HuesCore.js +++ b/src/js/HuesCore.js @@ -25,1028 +25,1030 @@ (function(window, document) { "use strict"; -function HuesCore(defaults) { - this.eventListeners = { - /* callback time(hundredths) - * - * When the song time is updated - negative for buildup - * Returns a floating point number denoting seconds - */ - time : [], - /* callback blurUpdate(xPercent, yPercent) - * - * The current blur amounts, in percent of full blur - */ - blurupdate : [], +class HuesCore { + constructor(defaults) { + this.eventListeners = { + /* callback time(hundredths) + * + * When the song time is updated - negative for buildup + * Returns a floating point number denoting seconds + */ + time : [], + /* callback blurUpdate(xPercent, yPercent) + * + * The current blur amounts, in percent of full blur + */ + blurupdate : [], + + /* callback newsong(song) + * + * Called on song change, whether user triggered or autosong. + * Song object is passed. + */ + newsong : [], + /* callback newimage(image) + * + * Called on image change, whether user triggered or FULL AUTO mode. + * Image object is passed. + */ + newimage : [], + /* callback newcolour(colour, isFade) + * + * Called on colour change. + * colour: colour object. + * isFade: if the colour is fading from the previous value + */ + newcolour : [], + /* callback newmode(mode) + * + * Called on mode change. + * Mode is passed as a boolean. + */ + newmode : [], + /* callback beat(beatString, beatIndex) + * + * Called on every new beat. + * beatString is a 256 char long array of current and upcoming beat chars + * beatIndex is the beat index. Negative during buildups + */ + beat : [], + /* callback invert(isInverted) + * + * Called whenever the invert state changes. + * Invert state is passed as a boolean. + */ + invert : [], + /* callback frame() + * + * Called on each new frame, at the end of all other frame processing + */ + frame : [], + /* callback songstarted() + * + * Called when the song actually begins to play, not just when the + * new song processing begins + */ + songstarted : [], + /* callback settingsupdated() + * + * Called when settings are updated and should be re-read from localStorage + */ + settingsupdated : [] + }; + + // Bunch-o-initialisers + this.version = 30; + this.versionStr = (this.version/10).toFixed(1); + this.versionHex = this.version.toString(16); + this.beatIndex = 0; - /* callback newsong(song) - * - * Called on song change, whether user triggered or autosong. - * Song object is passed. - */ - newsong : [], - /* callback newimage(image) - * - * Called on image change, whether user triggered or FULL AUTO mode. - * Image object is passed. - */ - newimage : [], - /* callback newcolour(colour, isFade) - * - * Called on colour change. - * colour: colour object. - * isFade: if the colour is fading from the previous value - */ - newcolour : [], - /* callback newmode(mode) - * - * Called on mode change. - * Mode is passed as a boolean. - */ - newmode : [], - /* callback beat(beatString, beatIndex) - * - * Called on every new beat. - * beatString is a 256 char long array of current and upcoming beat chars - * beatIndex is the beat index. Negative during buildups - */ - beat : [], - /* callback invert(isInverted) - * - * Called whenever the invert state changes. - * Invert state is passed as a boolean. - */ - invert : [], - /* callback frame() - * - * Called on each new frame, at the end of all other frame processing - */ - frame : [], - /* callback songstarted() - * - * Called when the song actually begins to play, not just when the - * new song processing begins - */ - songstarted : [], - /* callback settingsupdated() - * - * Called when settings are updated and should be re-read from localStorage - */ - settingsupdated : [] - }; - - // Bunch-o-initialisers - this.version = 30; - this.versionStr = (this.version/10).toFixed(1); - this.versionHex = this.version.toString(16); - this.beatIndex = 0; - - // How long a beat lasts for in each section - this.buildLength = -1; - this.loopLength = -1; - - this.currentSong = null; - this.currentImage = null; - this.songIndex = -1; - this.imageIndex = -1; - this.lastSongArray = []; - this.lastImageArray = []; - - this.colourIndex = 0x3f; - this.colours = this.oldColours; - - this.isFullAuto = true; - this.invert = false; - this.loopCount = 0; - this.doBuildup = true; - this.userInterface = null; - this.uiArray = []; - - // What's our root element? - this.root = null; - if(!defaults.root) { - this.root = document.body; - } else if(typeof defaults.root === "string") { - if(defaults.root && document.getElementById(defaults.root)) { - this.root = document.getElementById(defaults.root); - } else { + // How long a beat lasts for in each section + this.buildLength = -1; + this.loopLength = -1; + + this.currentSong = null; + this.currentImage = null; + this.songIndex = -1; + this.imageIndex = -1; + this.lastSongArray = []; + this.lastImageArray = []; + + this.colourIndex = 0x3f; + this.colours = HuesCore.oldColours; + + this.isFullAuto = true; + this.invert = false; + this.loopCount = 0; + this.doBuildup = true; + this.userInterface = null; + this.uiArray = []; + + // What's our root element? + this.root = null; + if(!defaults.root) { this.root = document.body; + } else if(typeof defaults.root === "string") { + if(defaults.root && document.getElementById(defaults.root)) { + this.root = document.getElementById(defaults.root); + } else { + this.root = document.body; + } + } else { // been given an element + this.root = defaults.root; + } + this.root.classList.add("hues-root"); + // Special case for full page Hues + if(this.root === document.body) { + document.documentElement.className = "hues-root"; + } + // Yes, we do indeed have Javascript + this.root.innerHTML = ""; + + this.makePreloader(this.root); + + window.onerror = (msg, url, line, col, error) => { + this.error(msg); + // Get more info in console + return false; + }; + + this.settings = new HuesSettings(defaults); + // Update with merged defaults + defaults = this.settings.defaults; + zip.workerScriptsPath = defaults.workersPath; + + this.window = new HuesWindow(this.root, defaults); + + console.log("0x40 Hues v" + this.versionStr + " - start your engines!"); + + this.resourceManager = new Resources(this, this.window); + this.editor = new HuesEditor(this, this.window); + this.settings.initUI(this.window); + populateHuesInfo(this.versionStr, this.window, defaults); + + this.window.selectTab(defaults.firstWindow, true); + + let ui = document.createElement("div"); + ui.className = "hues-ui"; + this.root.appendChild(ui); + this.uiArray.push(new RetroUI(ui), new WeedUI(ui), new ModernUI(ui), + new XmasUI(ui), new HalloweenUI(ui), new MinimalUI(ui)); + + this.autoSong = localStorage["autoSong"]; + + this.visualiser = document.createElement("canvas"); + this.visualiser.className = "hues-visualiser"; + this.visualiser.height = "64"; + this.vCtx = this.visualiser.getContext("2d"); + + this.soundManager = new SoundManager(this); + + this.soundManager.init().then(() => { + if(!this.soundManager.locked && localStorage["skipPreloader"] == "on") { + return null; + } else { + return this.resourceManager.getSizes(defaults.respacks); + } + }).then( sizes => { + if(sizes === null) { + return; + } + + let size = sizes.reduce( (prev, curr) => { + return typeof curr === 'number' ? prev + curr : null; + }, 0); + if(typeof size === 'number') { + size = size.toFixed(1); + } else { + size = '???'; + } + + let warning = size + "MB of music/images.
" + + "Flashing lights.
" + + "Tap or click to start"; + + if(!this.soundManager.locked) { + warning += "
Skip this screen from Options"; + } + this.warning(warning); + // Even if not locked, this steals clicks which is useful here + return this.soundManager.unlock(); + }).then(() => { + this.clearMessage(); + setInterval(this.loopCheck.bind(this), 1000); + this.renderer = new HuesCanvas(this.root, this.soundManager.context, this); + // Now all our objects are instantiated, we fire the updated settings + this.settings.addEventListener("updated", this.settingsUpdated.bind(this)); + this.settingsUpdated(); + this.setColour(this.colourIndex); + this.animationLoop(); + + if(defaults.load) { + return this.resourceManager.addAll(defaults.respacks, progress => { + this.preloader.style.backgroundPosition = (100 - progress*100) + "% 0%"; + let scale = Math.floor(progress * defaults.preloadMax); + let padding = defaults.preloadMax.toString(defaults.preloadBase).length; + this.preloadMsg.textContent = defaults.preloadPrefix + (Array(padding).join("0")+scale.toString(defaults.preloadBase)).slice(-padding); + }); + } else { + this.preloader.style.display = "none"; + return; + } + }).then(() => { + this.preloader.classList.add("hues-preloader--loaded"); + if(defaults.firstImage) { + this.setImageByName(defaults.firstImage); + } else { + this.setImage(0); + } + if(defaults.autoplay) { + if(defaults.firstSong) { + this.setSongByName(defaults.firstSong); + } else { + this.setSong(0); + } + } + }).catch(error => { // Comment this out to get proper stack traces + this.error(error); + }); + + if(!defaults.disableKeyboard) { + document.addEventListener("keydown", e => { + e = e || window.event; + if(e.defaultPrevented) { + return true; + } + // Ignore modifiers so we don't steal other events + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { + return true; + } + // If we've focused a text input, let the input go through! + if((e.target.tagName.toLowerCase() == "input" && e.target.type == "text") || + e.target.contentEditable === "true") { + return true; + } + let key = e.keyCode || e.which; + return this.keyHandler(key); + }); } - } else { // been given an element - this.root = defaults.root; } - this.root.classList.add("hues-root"); - // Special case for full page Hues - if(this.root === document.body) { - document.documentElement.className = "hues-root"; + + callEventListeners(ev) { + let args = Array.prototype.slice.call(arguments, 1); + this.eventListeners[ev].forEach(function(callback) { + callback.apply(null, args); + }); } - // Yes, we do indeed have Javascript - this.root.innerHTML = ""; - - this.makePreloader(this.root); - window.onerror = (msg, url, line, col, error) => { - this.error(msg); - // Get more info in console - return false; - }; - - this.settings = new HuesSettings(defaults); - // Update with merged defaults - defaults = this.settings.defaults; - zip.workerScriptsPath = defaults.workersPath; - - this.window = new HuesWindow(this.root, defaults); - - console.log("0x40 Hues v" + this.versionStr + " - start your engines!"); - - this.resourceManager = new Resources(this, this.window); - this.editor = new HuesEditor(this, this.window); - this.settings.initUI(this.window); - populateHuesInfo(this.versionStr, this.window, defaults); - - this.window.selectTab(defaults.firstWindow, true); + addEventListener(ev, callback) { + ev = ev.toLowerCase(); + if (typeof(this.eventListeners[ev]) !== "undefined") { + this.eventListeners[ev].push(callback); + } else { + throw Error("Unknown event: " + ev); + } + } - let ui = document.createElement("div"); - ui.className = "hues-ui"; - this.root.appendChild(ui); - this.uiArray.push(new RetroUI(ui), new WeedUI(ui), new ModernUI(ui), - new XmasUI(ui), new HalloweenUI(ui), new MinimalUI(ui)); - - this.autoSong = localStorage["autoSong"]; - - this.visualiser = document.createElement("canvas"); - this.visualiser.className = "hues-visualiser"; - this.visualiser.height = "64"; - this.vCtx = this.visualiser.getContext("2d"); - - this.soundManager = new SoundManager(this); - - this.soundManager.init().then(() => { - if(!this.soundManager.locked && localStorage["skipPreloader"] == "on") { - return null; + removeEventListener(ev, callback) { + ev = ev.toLowerCase(); + if (typeof(this.eventListeners[ev]) !== "undefined") { + this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { + return (a !== callback); + }); } else { - return this.resourceManager.getSizes(defaults.respacks); + throw Error("Unknown event: " + ev); + } + } + + makePreloader(root) { + this.preloader = document.createElement("div"); + this.preloader.className = "hues-preloader"; + root.appendChild(this.preloader); + + this.preloadMsg = document.createElement("div"); + this.preloadMsg.className = "hues-preloader__text"; + this.preloadMsg.textContent = "Initialising..."; + this.preloader.appendChild(this.preloadMsg); + + this.preloadSubMsg = document.createElement("div"); + this.preloadSubMsg.className = "hues-preloader__subtext"; + this.preloader.appendChild(this.preloadSubMsg); + } + + resizeVisualiser() { + this.soundManager.initVisualiser(this.visualiser.width/2); + } + + updateVisualiser() { + if(localStorage["visualiser"] != "on") { + return; } - }).then( sizes => { - if(sizes === null) { + + let logArrays = this.soundManager.getVisualiserData(); + if(!logArrays) { return; } + + this.vCtx.clearRect(0, 0, this.vCtx.canvas.width, this.vCtx.canvas.height); - let size = sizes.reduce( (prev, curr) => { - return typeof curr === 'number' ? prev + curr : null; - }, 0); - if(typeof size === 'number') { - size = size.toFixed(1); + let gradient=this.vCtx.createLinearGradient(0,64,0,0); + if(this.invert) { + gradient.addColorStop(1,"rgba(20,20,20,0.6)"); + gradient.addColorStop(0,"rgba(255,255,255,0.6)"); } else { - size = '???'; + gradient.addColorStop(1,"rgba(255,255,255,0.6)"); + gradient.addColorStop(0,"rgba(20,20,20,0.6)"); } + this.vCtx.fillStyle = gradient; - let warning = size + "MB of music/images.
" + - "Flashing lights.
" + - "Tap or click to start"; - - if(!this.soundManager.locked) { - warning += "
Skip this screen from Options"; + let barWidth = 2; + let barHeight; + let x = 0; + for(let a = 0; a < logArrays.length; a++) { + let vals = logArrays[a]; + for(let i = 0; i < vals.length; i++) { + let index = 0; + if(logArrays.length == 2 && a === 0) { + index = vals.length - i - 1; + } else { + index = i; + } + barHeight = vals[index]/4; + + this.vCtx.fillRect(x,this.vCtx.canvas.height-barHeight,barWidth,barHeight); + + x += barWidth; + } } - this.warning(warning); - // Even if not locked, this steals clicks which is useful here - return this.soundManager.unlock(); - }).then(() => { - this.clearMessage(); - setInterval(this.loopCheck.bind(this), 1000); - this.renderer = new HuesCanvas(this.root, this.soundManager.context, this); - // Now all our objects are instantiated, we fire the updated settings - this.settings.addEventListener("updated", this.settingsUpdated.bind(this)); - this.settingsUpdated(); - this.setColour(this.colourIndex); - this.animationLoop(); - - if(defaults.load) { - return this.resourceManager.addAll(defaults.respacks, progress => { - this.preloader.style.backgroundPosition = (100 - progress*100) + "% 0%"; - let scale = Math.floor(progress * defaults.preloadMax); - let padding = defaults.preloadMax.toString(defaults.preloadBase).length; - this.preloadMsg.textContent = defaults.preloadPrefix + (Array(padding).join("0")+scale.toString(defaults.preloadBase)).slice(-padding); - }); - } else { - this.preloader.style.display = "none"; + } + + animationLoop() { + requestAnimationFrame(this.animationLoop.bind(this)); + if(!this.soundManager.playing) { + this.callEventListeners("frame"); return; } - }).then(() => { - this.preloader.classList.add("hues-preloader--loaded"); - if(defaults.firstImage) { - this.setImageByName(defaults.firstImage); - } else { - this.setImage(0); + this.updateVisualiser(); + let now = this.soundManager.currentTime(); + this.callEventListeners("time", this.soundManager.clampedTime()); + if(now >= 0 && this.doBuildup) { + this.currentSong.buildupPlayed = true; } - if(defaults.autoplay) { - if(defaults.firstSong) { - this.setSongByName(defaults.firstSong); - } else { - this.setSong(0); - } + for(let beatTime = this.beatIndex * this.getBeatLength(); beatTime < now; + beatTime = ++this.beatIndex * this.getBeatLength()) { + let beat = this.getBeat(this.beatIndex); + this.beater(beat); } - }).catch(error => { // Comment this out to get proper stack traces - this.error(error); - }); - - if(!defaults.disableKeyboard) { - document.addEventListener("keydown", e => { - e = e || window.event; - if(e.defaultPrevented) { - return true; - } - // Ignore modifiers so we don't steal other events - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { - return true; - } - // If we've focused a text input, let the input go through! - if((e.target.tagName.toLowerCase() == "input" && e.target.type == "text") || - e.target.contentEditable === "true") { - return true; + this.callEventListeners("frame"); + } + + recalcBeatIndex(forcedNow) { + let now = typeof forcedNow === "number" ? forcedNow : this.soundManager.currentTime(); + // getBeatLength isn't updated with the right beatIndex yet + this.beatIndex = Math.floor(now / (now < 0 ? this.buildLength : this.loopLength)); + // beatIndex is NaN, abort + if(this.beatIndex != this.beatIndex) { + this.setInvert(false); + return; + } + + // We should sync up to how many inverts there are + let build = this.currentSong.buildupRhythm; + let rhythm = this.currentSong.rhythm; + let mapSoFar; + if(this.beatIndex < 0) { + // Clamp to 0 in case we've juuust started + mapSoFar = build.slice(0, Math.max(this.beatIndex + build.length, 0)); + } else { + // If the rhythm has an odd number of inverts, don't reset because it + // alternates on each loop anyway + if((rhythm.match(/i|I/g)||[]).length % 2) { + return; } - let key = e.keyCode || e.which; - return this.keyHandler(key); - }); + mapSoFar = (build ? build : "") + rhythm.slice(0, this.beatIndex); + } + // If there's an odd amount of inverts thus far, invert our display + let invertCount = (mapSoFar.match(/i|I/g)||[]).length; + this.setInvert(invertCount % 2); } -} -HuesCore.prototype.callEventListeners = function(ev) { - let args = Array.prototype.slice.call(arguments, 1); - this.eventListeners[ev].forEach(function(callback) { - callback.apply(null, args); - }); -}; - -HuesCore.prototype.addEventListener = function(ev, callback) { - ev = ev.toLowerCase(); - if (typeof(this.eventListeners[ev]) !== "undefined") { - this.eventListeners[ev].push(callback); - } else { - throw Error("Unknown event: " + ev); + getBeatIndex() { + if(!this.soundManager.playing) { + return 0; + } else if(this.beatIndex < 0) { + return this.beatIndex; + } else { + return this.beatIndex % this.currentSong.rhythm.length; + } } -}; -HuesCore.prototype.removeEventListener = function(ev, callback) { - ev = ev.toLowerCase(); - if (typeof(this.eventListeners[ev]) !== "undefined") { - this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { - return (a !== callback); - }); - } else { - throw Error("Unknown event: " + ev); + getSafeBeatIndex() { + let index = this.getBeatIndex(); + if(index < 0) { + return 0; + } else { + return index; + } } -}; -HuesCore.prototype.makePreloader = function(root) { - this.preloader = document.createElement("div"); - this.preloader.className = "hues-preloader"; - root.appendChild(this.preloader); - - this.preloadMsg = document.createElement("div"); - this.preloadMsg.className = "hues-preloader__text"; - this.preloadMsg.textContent = "Initialising..."; - this.preloader.appendChild(this.preloadMsg); - - this.preloadSubMsg = document.createElement("div"); - this.preloadSubMsg.className = "hues-preloader__subtext"; - this.preloader.appendChild(this.preloadSubMsg); -}; - -HuesCore.prototype.resizeVisualiser = function() { - this.soundManager.initVisualiser(this.visualiser.width/2); -}; - -HuesCore.prototype.updateVisualiser = function() { - if(localStorage["visualiser"] != "on") { - return; + blurUpdated(x, y) { + this.callEventListeners("blurupdate", x, y); } - - let logArrays = this.soundManager.getVisualiserData(); - if(!logArrays) { - return; + + nextSong() { + let index = (this.songIndex + 1) % this.resourceManager.enabledSongs.length; + this.setSong(index); } - this.vCtx.clearRect(0, 0, this.vCtx.canvas.width, this.vCtx.canvas.height); - - let gradient=this.vCtx.createLinearGradient(0,64,0,0); - if(this.invert) { - gradient.addColorStop(1,"rgba(20,20,20,0.6)"); - gradient.addColorStop(0,"rgba(255,255,255,0.6)"); - } else { - gradient.addColorStop(1,"rgba(255,255,255,0.6)"); - gradient.addColorStop(0,"rgba(20,20,20,0.6)"); + previousSong() { + let index = ((this.songIndex - 1) + this.resourceManager.enabledSongs.length) % this.resourceManager.enabledSongs.length; + this.setSong(index); } - this.vCtx.fillStyle = gradient; - - let barWidth = 2; - let barHeight; - let x = 0; - for(let a = 0; a < logArrays.length; a++) { - let vals = logArrays[a]; - for(let i = 0; i < vals.length; i++) { - let index = 0; - if(logArrays.length == 2 && a === 0) { - index = vals.length - i - 1; - } else { - index = i; - } - barHeight = vals[index]/4; - - this.vCtx.fillRect(x,this.vCtx.canvas.height-barHeight,barWidth,barHeight); - x += barWidth; + setSongByName(name) { + let songs = this.resourceManager.enabledSongs; + for(let i = 0; i < songs.length; i++) { + if(songs[i].title == name) { + return this.setSong(i); + } } + return this.setSong(0); // fallback } -}; -HuesCore.prototype.animationLoop = function() { - requestAnimationFrame(this.animationLoop.bind(this)); - if(!this.soundManager.playing) { - this.callEventListeners("frame"); - return; - } - this.updateVisualiser(); - let now = this.soundManager.currentTime(); - this.callEventListeners("time", this.soundManager.clampedTime()); - if(now >= 0 && this.doBuildup) { - this.currentSong.buildupPlayed = true; - } - for(let beatTime = this.beatIndex * this.getBeatLength(); beatTime < now; - beatTime = ++this.beatIndex * this.getBeatLength()) { - let beat = this.getBeat(this.beatIndex); - this.beater(beat); - } - this.callEventListeners("frame"); -}; - -HuesCore.prototype.recalcBeatIndex = function(forcedNow) { - let now = typeof forcedNow === "number" ? forcedNow : this.soundManager.currentTime(); - // getBeatLength isn't updated with the right beatIndex yet - this.beatIndex = Math.floor(now / (now < 0 ? this.buildLength : this.loopLength)); - // beatIndex is NaN, abort - if(this.beatIndex != this.beatIndex) { - this.setInvert(false); - return; + /* To set songs via reference instead of index - used in HuesEditor */ + setSongOject(song) { + for(let i = 0; i < this.resourceManager.enabledSongs.length; i++) { + if(this.resourceManager.enabledSongs[i] === song) { + return this.setSong(i); + } + } } - - // We should sync up to how many inverts there are - let build = this.currentSong.buildupRhythm; - let rhythm = this.currentSong.rhythm; - let mapSoFar; - if(this.beatIndex < 0) { - // Clamp to 0 in case we've juuust started - mapSoFar = build.slice(0, Math.max(this.beatIndex + build.length, 0)); - } else { - // If the rhythm has an odd number of inverts, don't reset because it - // alternates on each loop anyway - if((rhythm.match(/i|I/g)||[]).length % 2) { + + setSong(index, leaveArray) { + if(this.currentSong == this.resourceManager.enabledSongs[index]) { return; } - mapSoFar = (build ? build : "") + rhythm.slice(0, this.beatIndex); - } - // If there's an odd amount of inverts thus far, invert our display - let invertCount = (mapSoFar.match(/i|I/g)||[]).length; - this.setInvert(invertCount % 2); -}; - -HuesCore.prototype.getBeatIndex = function() { - if(!this.soundManager.playing) { - return 0; - } else if(this.beatIndex < 0) { - return this.beatIndex; - } else { - return this.beatIndex % this.currentSong.rhythm.length; + // When not randoming, clear this + if(!leaveArray) { + this.lastSongArray = []; + } + this.lastSongArray.push(index); + this.songIndex = index; + this.currentSong = this.resourceManager.enabledSongs[this.songIndex]; + if (this.currentSong === undefined) { + this.currentSong = {"name":"None", "title":"None", "rhythm":".", "source":null, "crc":"none", "sound":null, "enabled":true, "filename":"none"}; + } + console.log("Next song:", this.songIndex, this.currentSong); + this.callEventListeners("newsong", this.currentSong); + this.loopCount = 0; + if (this.currentSong.buildup) { + switch (localStorage["playBuildups"]) { + case "off": + this.currentSong.buildupPlayed = true; + this.doBuildup = false; + break; + case "on": + this.currentSong.buildupPlayed = false; + this.doBuildup = true; + break; + case "once": + this.doBuildup = !this.currentSong.buildupPlayed; + break; + } + } + this.setInvert(false); + this.renderer.doBlackout(); + return this.soundManager.playSong(this.currentSong, this.doBuildup) + .then(() => { + this.resetAudio(); + this.fillBuildup(); + this.callEventListeners("songstarted"); + }); } -}; -HuesCore.prototype.getSafeBeatIndex = function() { - let index = this.getBeatIndex(); - if(index < 0) { - return 0; - } else { - return index; - } -}; - -HuesCore.prototype.blurUpdated = function(x, y) { - this.callEventListeners("blurupdate", x, y); -}; - -HuesCore.prototype.nextSong = function() { - let index = (this.songIndex + 1) % this.resourceManager.enabledSongs.length; - this.setSong(index); -}; - -HuesCore.prototype.previousSong = function() { - let index = ((this.songIndex - 1) + this.resourceManager.enabledSongs.length) % this.resourceManager.enabledSongs.length; - this.setSong(index); -}; - -HuesCore.prototype.setSongByName = function(name) { - let songs = this.resourceManager.enabledSongs; - for(let i = 0; i < songs.length; i++) { - if(songs[i].title == name) { - return this.setSong(i); + updateBeatLength() { + this.loopLength = this.soundManager.loopLength / this.currentSong.rhythm.length; + if(this.currentSong.buildup) { + if (!this.currentSong.buildupRhythm) { + this.currentSong.buildupRhythm = "."; + } + this.buildLength = this.soundManager.buildLength / this.currentSong.buildupRhythm.length; + } else { + this.buildLength = -1; } } - return this.setSong(0); // fallback -}; - -/* To set songs via reference instead of index - used in HuesEditor */ -HuesCore.prototype.setSongOject = function(song) { - for(let i = 0; i < this.resourceManager.enabledSongs.length; i++) { - if(this.resourceManager.enabledSongs[i] === song) { - return this.setSong(i); + + getBeatLength() { + if(this.beatIndex < 0) { + return this.buildLength; + } else { + return this.loopLength; } } -}; -HuesCore.prototype.setSong = function(index, leaveArray) { - if(this.currentSong == this.resourceManager.enabledSongs[index]) { - return; + fillBuildup() { + // update loop length for flash style filling + this.updateBeatLength(); + if(this.currentSong.buildup) { + if(this.currentSong.independentBuild) { + console.log("New behaviour - separate build/loop lengths"); + // Do nothing + } else { + console.log("Flash behaviour - filling buildup"); + let buildBeats = Math.floor(this.soundManager.buildLength / this.loopLength); + if(buildBeats < 1) { + buildBeats = 1; + } + while (this.currentSong.buildupRhythm.length < buildBeats) { + this.currentSong.buildupRhythm = this.currentSong.buildupRhythm + "."; + } + console.log("Buildup length:", buildBeats); + } + } + // update with a buildup of possibly different length + this.updateBeatLength(); + // If we're in the build or loop this will adjust + // If we've lagged a bit, we'll miss the first beat. Rewind! + this.recalcBeatIndex(this.doBuildup ? -this.soundManager.buildLength : 0); } - // When not randoming, clear this - if(!leaveArray) { - this.lastSongArray = []; + + randomSong() { + let songCount = this.resourceManager.enabledSongs.length; + let index=Math.floor((Math.random() * songCount)); + if (songCount > 1 && (index == this.songIndex || this.lastSongArray.indexOf(index) != -1)) { + this.randomSong(); + } else { + console.log("Randoming a song!"); + this.setSong(index, true); + let noRepeat = Math.min(5, Math.floor(songCount / 2)); + while (this.lastSongArray.length > noRepeat && noRepeat >= 0) { + this.lastSongArray.shift(); + } + } } - this.lastSongArray.push(index); - this.songIndex = index; - this.currentSong = this.resourceManager.enabledSongs[this.songIndex]; - if (this.currentSong === undefined) { - this.currentSong = {"name":"None", "title":"None", "rhythm":".", "source":null, "crc":"none", "sound":null, "enabled":true, "filename":"none"}; + + /* This is its own function because requestAnimationFrame is called very very + rarely when the tab is backgrounded. As autoSong is often used to chill with + music, it's important to keep checking the loop so songs don't go for too + long. */ + loopCheck() { + if(Math.floor(this.soundManager.currentTime() / this.soundManager.loopLength) > this.loopCount) { + this.onLoop(); + } } - console.log("Next song:", this.songIndex, this.currentSong); - this.callEventListeners("newsong", this.currentSong); - this.loopCount = 0; - if (this.currentSong.buildup) { - switch (localStorage["playBuildups"]) { - case "off": - this.currentSong.buildupPlayed = true; - this.doBuildup = false; - break; - case "on": - this.currentSong.buildupPlayed = false; - this.doBuildup = true; + + onLoop() { + this.loopCount++; + switch (localStorage["autoSong"]) { + case "loop": + console.log("Checking loops"); + if (this.loopCount >= localStorage["autoSongDelay"]) { + this.doAutoSong(); + } break; - case "once": - this.doBuildup = !this.currentSong.buildupPlayed; + case "time": + console.log("Checking times"); + if (this.soundManager.loopLength * this.loopCount >= localStorage["autoSongDelay"] * 60) { + this.doAutoSong(); + } break; } } - this.setInvert(false); - this.renderer.doBlackout(); - return this.soundManager.playSong(this.currentSong, this.doBuildup) - .then(() => { - this.resetAudio(); - this.fillBuildup(); - this.callEventListeners("songstarted"); - }); -}; - -HuesCore.prototype.updateBeatLength = function() { - this.loopLength = this.soundManager.loopLength / this.currentSong.rhythm.length; - if(this.currentSong.buildup) { - if (!this.currentSong.buildupRhythm) { - this.currentSong.buildupRhythm = "."; + + doAutoSong() { + let func = null; + if(localStorage["autoSongShuffle"] == "on") { + func = this.randomSong; + } else { + func = this.nextSong; + } + if(localStorage["autoSongFadeout"] == "on") { + this.soundManager.fadeOut(() => { + func.call(this); + }); + } else { + func.call(this); } - this.buildLength = this.soundManager.buildLength / this.currentSong.buildupRhythm.length; - } else { - this.buildLength = -1; } -}; -HuesCore.prototype.getBeatLength = function() { - if(this.beatIndex < 0) { - return this.buildLength; - } else { - return this.loopLength; - } -}; - -HuesCore.prototype.fillBuildup = function() { - // update loop length for flash style filling - this.updateBeatLength(); - if(this.currentSong.buildup) { - if(this.currentSong.independentBuild) { - console.log("New behaviour - separate build/loop lengths"); - // Do nothing - } else { - console.log("Flash behaviour - filling buildup"); - let buildBeats = Math.floor(this.soundManager.buildLength / this.loopLength); - if(buildBeats < 1) { - buildBeats = 1; - } - while (this.currentSong.buildupRhythm.length < buildBeats) { - this.currentSong.buildupRhythm = this.currentSong.buildupRhythm + "."; - } - console.log("Buildup length:", buildBeats); + songDataUpdated() { + if (this.currentSong) { + this.callEventListeners("newsong", this.currentSong); + this.callEventListeners("newimage", this.currentImage); } } - // update with a buildup of possibly different length - this.updateBeatLength(); - // If we're in the build or loop this will adjust - // If we've lagged a bit, we'll miss the first beat. Rewind! - this.recalcBeatIndex(this.doBuildup ? -this.soundManager.buildLength : 0); -}; - -HuesCore.prototype.randomSong = function() { - let songCount = this.resourceManager.enabledSongs.length; - let index=Math.floor((Math.random() * songCount)); - if (songCount > 1 && (index == this.songIndex || this.lastSongArray.indexOf(index) != -1)) { - this.randomSong(); - } else { - console.log("Randoming a song!"); - this.setSong(index, true); - let noRepeat = Math.min(5, Math.floor(songCount / 2)); - while (this.lastSongArray.length > noRepeat && noRepeat >= 0) { - this.lastSongArray.shift(); + + resetAudio() { + this.beatIndex = 0; + this.songDataUpdated(); + if(localStorage["visualiser"] == "on") { + this.soundManager.initVisualiser(this.visualiser.width/2); } } -}; - -/* This is its own function because requestAnimationFrame is called very very - rarely when the tab is backgrounded. As autoSong is often used to chill with - music, it's important to keep checking the loop so songs don't go for too - long. */ -HuesCore.prototype.loopCheck = function() { - if(Math.floor(this.soundManager.currentTime() / this.soundManager.loopLength) > this.loopCount) { - this.onLoop(); + + randomImage() { + if(localStorage["shuffleImages"] == "on") { + let len = this.resourceManager.enabledImages.length; + let index = Math.floor(Math.random() * len); + if ((index == this.imageIndex || this.lastImageArray.indexOf(index) != -1) && len > 1) { + this.randomImage(); + } else { + this.setImage(index, true); + this.lastImageArray.push(index); + let cull = Math.min(20, Math.floor((len / 2))); + while (this.lastImageArray.length > cull && cull >= 0) { + this.lastImageArray.shift(); + } + } + } else { // jk, not actually random + let img=(this.imageIndex + 1) % this.resourceManager.enabledImages.length; + this.setImage(img); + } } -}; - -HuesCore.prototype.onLoop = function() { - this.loopCount++; - switch (localStorage["autoSong"]) { - case "loop": - console.log("Checking loops"); - if (this.loopCount >= localStorage["autoSongDelay"]) { - this.doAutoSong(); + + setImage(index, leaveArray) { + // If there are no images, this corrects NaN to 0 + this.imageIndex = index ? index : 0; + let img=this.resourceManager.enabledImages[this.imageIndex]; + if (img == this.currentImage && img !== null) { + return; + } + // When not randoming, clear this + if(!leaveArray) { + this.lastImageArray = []; } - break; - case "time": - console.log("Checking times"); - if (this.soundManager.loopLength * this.loopCount >= localStorage["autoSongDelay"] * 60) { - this.doAutoSong(); + if (img) { + this.currentImage = img; + } else { + this.currentImage = {"name":"None", "fullname":"None", "align":"center", "bitmap":null, "source":null, "enabled":true}; + this.imageIndex = -1; + this.lastImageArray = []; } - break; + this.callEventListeners("newimage", this.currentImage); } -}; - -HuesCore.prototype.doAutoSong = function() { - let func = null; - if(localStorage["autoSongShuffle"] == "on") { - func = this.randomSong; - } else { - func = this.nextSong; + + setImageByName(name) { + let images = this.resourceManager.enabledImages; + for(let i = 0; i < images.length; i++) { + if(images[i].name == name || images[i].fullname == name) { + this.setImage(i); + return; + } + } + this.setImage(0); // fallback } - if(localStorage["autoSongFadeout"] == "on") { - this.soundManager.fadeOut(() => { - func.call(this); - }); - } else { - func.call(this); + + nextImage() { + this.setIsFullAuto(false); + let img=(this.imageIndex + 1) % this.resourceManager.enabledImages.length; + this.setImage(img); } -}; -HuesCore.prototype.songDataUpdated = function() { - if (this.currentSong) { - this.callEventListeners("newsong", this.currentSong); - this.callEventListeners("newimage", this.currentImage); + previousImage() { + this.setIsFullAuto(false); + let img=((this.imageIndex - 1) + this.resourceManager.enabledImages.length) % this.resourceManager.enabledImages.length; + this.setImage(img); } -}; -HuesCore.prototype.resetAudio = function() { - this.beatIndex = 0; - this.songDataUpdated(); - if(localStorage["visualiser"] == "on") { - this.soundManager.initVisualiser(this.visualiser.width/2); + randomColourIndex() { + let index=Math.floor((Math.random() * this.colours.length)); + if (index == this.colourIndex) { + return this.randomColourIndex(); + } + return index; + } + + randomColour(isFade) { + let index=this.randomColourIndex(); + this.setColour(index, isFade); } -}; - -HuesCore.prototype.randomImage = function() { - if(localStorage["shuffleImages"] == "on") { - let len = this.resourceManager.enabledImages.length; - let index = Math.floor(Math.random() * len); - if ((index == this.imageIndex || this.lastImageArray.indexOf(index) != -1) && len > 1) { - this.randomImage(); + + setColour(index, isFade) { + this.colourIndex = index; + let colour = this.colours[this.colourIndex]; + this.callEventListeners("newcolour", colour, isFade); + } + + getBeat(index) { + if(index < 0) { + return this.currentSong.buildupRhythm[this.currentSong.buildupRhythm.length+index]; } else { - this.setImage(index, true); - this.lastImageArray.push(index); - let cull = Math.min(20, Math.floor((len / 2))); - while (this.lastImageArray.length > cull && cull >= 0) { - this.lastImageArray.shift(); + return this.currentSong.rhythm[index % this.currentSong.rhythm.length]; + } + } + + beater(beat) { + this.callEventListeners("beat", this.getBeatString(), this.getBeatIndex()); + switch(beat) { + case 'X': + case 'x': + this.renderer.doYBlur(); + break; + case 'O': + case 'o': + this.renderer.doXBlur(); + break; + case ')': + this.renderer.doTrippyX(); + break; + case '(': + this.renderer.doTrippyY(); + break; + case '+': + this.renderer.doXBlur(); + this.renderer.doBlackout(); + break; + case '¤': + this.renderer.doXBlur(); + this.renderer.doBlackout(true); + break; + case '|': + this.renderer.doShortBlackout(this.getBeatLength()); + this.randomColour(); + break; + case ':': + this.randomColour(); + break; + case '*': + if(this.isFullAuto) { + this.randomImage(); + } + break; + case '=': + if(this.isFullAuto) { + this.randomImage(); + } + /* falls through */ + case '~': + // case: fade in build, not in rhythm. Must max out fade timer. + let maxSearch = this.currentSong.rhythm.length; + if(this.beatIndex < 0) { + maxSearch -= this.beatIndex; + } + let fadeLen; + for (fadeLen = 1; fadeLen <= maxSearch; fadeLen++) { + if (this.getBeat(fadeLen + this.beatIndex) != ".") { + break; + } + } + this.renderer.doColourFade((fadeLen * this.getBeatLength()) / this.soundManager.playbackRate); + this.randomColour(true); + break; + case 'I': + if (this.isFullAuto) { + this.randomImage(); + } + /* falls through */ + case 'i': + this.toggleInvert(); + break; + } + if ([".", "+", "|", "¤"].indexOf(beat) == -1) { + this.renderer.clearBlackout(); } + if([".", "+", "¤", ":", "*", "X", "O", "~", "=", "i", "I"].indexOf(beat) == -1) { + this.randomColour(); + if (this.isFullAuto) { + this.randomImage(); + } } - } else { // jk, not actually random - let img=(this.imageIndex + 1) % this.resourceManager.enabledImages.length; - this.setImage(img); } -}; - -HuesCore.prototype.setImage = function(index, leaveArray) { - // If there are no images, this corrects NaN to 0 - this.imageIndex = index ? index : 0; - let img=this.resourceManager.enabledImages[this.imageIndex]; - if (img == this.currentImage && img !== null) { - return; + + getBeatString(length) { + length = length ? length : 256; + + let beatString = ""; + let song = this.currentSong; + if (song) { + if(this.beatIndex < 0) { + beatString = song.buildupRhythm.slice( + song.buildupRhythm.length + this.beatIndex); + } else { + beatString = song.rhythm.slice(this.beatIndex % song.rhythm.length); + } + while (beatString.length < length) { + beatString += song.rhythm; + } + } + + return beatString; } - // When not randoming, clear this - if(!leaveArray) { - this.lastImageArray = []; + + setIsFullAuto(auto) { + this.isFullAuto = auto; + if (this.userInterface) { + this.callEventListeners("newmode", this.isFullAuto); + } } - if (img) { - this.currentImage = img; - } else { - this.currentImage = {"name":"None", "fullname":"None", "align":"center", "bitmap":null, "source":null, "enabled":true}; - this.imageIndex = -1; - this.lastImageArray = []; + + toggleFullAuto() { + this.setIsFullAuto(!this.isFullAuto); } - this.callEventListeners("newimage", this.currentImage); -}; - -HuesCore.prototype.setImageByName = function(name) { - let images = this.resourceManager.enabledImages; - for(let i = 0; i < images.length; i++) { - if(images[i].name == name || images[i].fullname == name) { - this.setImage(i); - return; - } + + setInvert(invert) { + this.invert = !!invert; + this.callEventListeners("invert", invert); } - this.setImage(0); // fallback -}; - -HuesCore.prototype.nextImage = function() { - this.setIsFullAuto(false); - let img=(this.imageIndex + 1) % this.resourceManager.enabledImages.length; - this.setImage(img); -}; - -HuesCore.prototype.previousImage = function() { - this.setIsFullAuto(false); - let img=((this.imageIndex - 1) + this.resourceManager.enabledImages.length) % this.resourceManager.enabledImages.length; - this.setImage(img); -}; - -HuesCore.prototype.randomColourIndex = function() { - let index=Math.floor((Math.random() * this.colours.length)); - if (index == this.colourIndex) { - return this.randomColourIndex(); + + toggleInvert() { + this.setInvert(!this.invert); + } + + respackLoaded() { + this.init(); } - return index; -}; - -HuesCore.prototype.randomColour = function(isFade) { - let index=this.randomColourIndex(); - this.setColour(index, isFade); -}; - -HuesCore.prototype.setColour = function(index, isFade) { - this.colourIndex = index; - let colour = this.colours[this.colourIndex]; - this.callEventListeners("newcolour", colour, isFade); -}; - -HuesCore.prototype.getBeat = function(index) { - if(index < 0) { - return this.currentSong.buildupRhythm[this.currentSong.buildupRhythm.length+index]; - } else { - return this.currentSong.rhythm[index % this.currentSong.rhythm.length]; + + changeUI(index) { + if (index >= 0 && this.uiArray.length > index && this.userInterface != this.uiArray[index]) { + this.hideLists(); + if(this.userInterface) { + this.userInterface.disconnect(); + } + this.userInterface = this.uiArray[index]; + this.userInterface.connectCore(this); + this.callEventListeners("newmode", this.isFullAuto); + this.callEventListeners("newsong", this.currentSong); + this.callEventListeners("newimage", this.currentImage); + this.callEventListeners("newcolour", this.colours[this.colourIndex], false); + this.callEventListeners("beat", this.getBeatString(), this.getBeatIndex()); + this.callEventListeners("invert", this.invert); + } } -}; - -HuesCore.prototype.beater = function(beat) { - this.callEventListeners("beat", this.getBeatString(), this.getBeatIndex()); - switch(beat) { - case 'X': - case 'x': - this.renderer.doYBlur(); + + settingsUpdated() { + this.callEventListeners("settingsupdated"); + switch (localStorage["currentUI"]) { + case "retro": + this.changeUI(0); break; - case 'O': - case 'o': - this.renderer.doXBlur(); + case "v4.20": + this.changeUI(1); break; - case ')': - this.renderer.doTrippyX(); + case "modern": + this.changeUI(2); break; - case '(': - this.renderer.doTrippyY(); + case "xmas": + this.changeUI(3); break; - case '+': - this.renderer.doXBlur(); - this.renderer.doBlackout(); + case "hlwn": + this.changeUI(4); break; - case '¤': - this.renderer.doXBlur(); - this.renderer.doBlackout(true); + case "mini": + this.changeUI(5); break; - case '|': - this.renderer.doShortBlackout(this.getBeatLength()); - this.randomColour(); + } + switch (localStorage["colourSet"]) { + case "normal": + this.colours = HuesCore.oldColours; break; - case ':': - this.randomColour(); + case "pastel": + this.colours = HuesCore.pastelColours; break; - case '*': - if(this.isFullAuto) { - this.randomImage(); - } + case "v4.20": + this.colours = HuesCore.weedColours; break; - case '=': - if(this.isFullAuto) { - this.randomImage(); - } - /* falls through */ - case '~': - // case: fade in build, not in rhythm. Must max out fade timer. - let maxSearch = this.currentSong.rhythm.length; - if(this.beatIndex < 0) { - maxSearch -= this.beatIndex; - } - let fadeLen; - for (fadeLen = 1; fadeLen <= maxSearch; fadeLen++) { - if (this.getBeat(fadeLen + this.beatIndex) != ".") { - break; - } + } + switch (localStorage["blackoutUI"]) { + case "off": + this.userInterface.show(); + break; + case "on": + if(this.renderer.blackout) { + this.userInterface.hide(); } - this.renderer.doColourFade((fadeLen * this.getBeatLength()) / this.soundManager.playbackRate); - this.randomColour(true); break; - case 'I': - if (this.isFullAuto) { - this.randomImage(); + } + switch (localStorage["visualiser"]) { + case "off": + this.visualiser.classList.add("hidden"); + break; + case "on": + this.visualiser.classList.remove("hidden"); + if(!this.soundManager.vReady) { + this.soundManager.initVisualiser(this.visualiser.width/2); } - /* falls through */ - case 'i': - this.toggleInvert(); break; } - if ([".", "+", "|", "¤"].indexOf(beat) == -1) { - this.renderer.clearBlackout(); + if (this.autoSong == "off" && localStorage["autoSong"] != "off") { + console.log("Resetting loopCount since AutoSong was enabled"); + this.loopCount = 0; } - if([".", "+", "¤", ":", "*", "X", "O", "~", "=", "i", "I"].indexOf(beat) == -1) { - this.randomColour(); - if (this.isFullAuto) { - this.randomImage(); - } + this.autoSong = localStorage["autoSong"]; } -}; -HuesCore.prototype.getBeatString = function(length) { - length = length ? length : 256; + enabledChanged() { + this.resourceManager.rebuildEnabled(); + } - let beatString = ""; - let song = this.currentSong; - if (song) { - if(this.beatIndex < 0) { - beatString = song.buildupRhythm.slice( - song.buildupRhythm.length + this.beatIndex); - } else { - beatString = song.rhythm.slice(this.beatIndex % song.rhythm.length); - } - while (beatString.length < length) { - beatString += song.rhythm; - } + hideLists() { + this.resourceManager.hideLists(); } - return beatString; -}; + toggleSongList() { + this.window.hide(); + this.resourceManager.toggleSongList(); + } -HuesCore.prototype.setIsFullAuto = function(auto) { - this.isFullAuto = auto; - if (this.userInterface) { - this.callEventListeners("newmode", this.isFullAuto); + toggleImageList() { + this.window.hide(); + this.resourceManager.toggleImageList(); } -}; - -HuesCore.prototype.toggleFullAuto = function() { - this.setIsFullAuto(!this.isFullAuto); -}; - -HuesCore.prototype.setInvert = function(invert) { - this.invert = !!invert; - this.callEventListeners("invert", invert); -}; - -HuesCore.prototype.toggleInvert = function() { - this.setInvert(!this.invert); -}; - -HuesCore.prototype.respackLoaded = function() { - this.init(); -}; - -HuesCore.prototype.changeUI = function(index) { - if (index >= 0 && this.uiArray.length > index && this.userInterface != this.uiArray[index]) { - this.hideLists(); - if(this.userInterface) { - this.userInterface.disconnect(); + + openSongSource() { + if (this.currentSong && this.currentSong.source) { + window.open(this.currentSong.source,'_blank'); } - this.userInterface = this.uiArray[index]; - this.userInterface.connectCore(this); - this.callEventListeners("newmode", this.isFullAuto); - this.callEventListeners("newsong", this.currentSong); - this.callEventListeners("newimage", this.currentImage); - this.callEventListeners("newcolour", this.colours[this.colourIndex], false); - this.callEventListeners("beat", this.getBeatString(), this.getBeatIndex()); - this.callEventListeners("invert", this.invert); - } -}; - -HuesCore.prototype.settingsUpdated = function() { - this.callEventListeners("settingsupdated"); - switch (localStorage["currentUI"]) { - case "retro": - this.changeUI(0); - break; - case "v4.20": - this.changeUI(1); - break; - case "modern": - this.changeUI(2); - break; - case "xmas": - this.changeUI(3); - break; - case "hlwn": - this.changeUI(4); - break; - case "mini": - this.changeUI(5); - break; } - switch (localStorage["colourSet"]) { - case "normal": - this.colours = this.oldColours; - break; - case "pastel": - this.colours = this.pastelColours; - break; - case "v4.20": - this.colours = this.weedColours; - break; - } - switch (localStorage["blackoutUI"]) { - case "off": - this.userInterface.show(); - break; - case "on": - if(this.renderer.blackout) { - this.userInterface.hide(); + + openImageSource() { + if (this.currentImage && this.currentImage.source) { + window.open(this.currentImage.source,'_blank'); } - break; } - switch (localStorage["visualiser"]) { - case "off": - this.visualiser.classList.add("hidden"); - break; - case "on": - this.visualiser.classList.remove("hidden"); - if(!this.soundManager.vReady) { - this.soundManager.initVisualiser(this.visualiser.width/2); + + keyHandler(key) { + switch (key) { + case 37: // LEFT + this.previousImage(); + break; + case 39: // RIGHT + this.nextImage(); + break; + case 38: // UP + this.nextSong(); + break; + case 40: // DOWN + this.previousSong(); + break; + case 70: // F + this.setIsFullAuto(!this.isFullAuto); + break; + case 109: // NUMPAD_SUBTRACT + case 189: // MINUS + case 173: // MINUS, legacy + this.soundManager.decreaseVolume(); + break; + case 107: // NUMPAD_ADD + case 187: // EQUAL + case 61: // EQUAL, legacy + this.soundManager.increaseVolume(); + break; + case 66: // B + this.soundManager.seek(-this.soundManager.buildLength); + break; + case 77: // M + this.soundManager.toggleMute(); + break; + case 72: // H + this.userInterface.toggleHide(); + break; + case 82: // R + this.window.selectTab("RESOURCES"); + break; + case 69: // E + this.window.selectTab("EDITOR"); + break; + case 79: // O + this.window.selectTab("OPTIONS"); + break; + case 73: // I + this.window.selectTab("INFO"); + break; + case 49: // NUMBER_1 + this.settings.set("currentUI", "retro"); + break; + case 50: // NUMBER_2 + this.settings.set("currentUI", "v4.20"); + break; + case 51: // NUMBER_3 + this.settings.set("currentUI", "modern"); + break; + case 52: // NUMBER_4 + this.settings.set("currentUI", "xmas"); + break; + case 53: // NUMBER_5 + this.settings.set("currentUI", "hlwn"); + break; + case 54: // NUMBER_6 + this.settings.set("currentUI", "mini"); + break; + case 67: // C + this.toggleImageList(); + break; + case 83: // S + this.toggleSongList(); + break; + case 87: // W + this.window.toggle(); + break; + case 78: // N + this.randomSong(); + break; + default: + return true; } - break; - } - if (this.autoSong == "off" && localStorage["autoSong"] != "off") { - console.log("Resetting loopCount since AutoSong was enabled"); - this.loopCount = 0; + return false; } - this.autoSong = localStorage["autoSong"]; -}; - -HuesCore.prototype.enabledChanged = function() { - this.resourceManager.rebuildEnabled(); -}; - -HuesCore.prototype.hideLists = function() { - this.resourceManager.hideLists(); -}; - -HuesCore.prototype.toggleSongList = function() { - this.window.hide(); - this.resourceManager.toggleSongList(); -}; - -HuesCore.prototype.toggleImageList = function() { - this.window.hide(); - this.resourceManager.toggleImageList(); -}; - -HuesCore.prototype.openSongSource = function() { - if (this.currentSong && this.currentSong.source) { - window.open(this.currentSong.source,'_blank'); + + error(message) { + console.log(message); + this.preloadSubMsg.textContent = message; + this.preloadMsg.style.color = "#F00"; } -}; -HuesCore.prototype.openImageSource = function() { - if (this.currentImage && this.currentImage.source) { - window.open(this.currentImage.source,'_blank'); + warning(message) { + console.log(message); + this.preloadSubMsg.innerHTML = message; + this.preloadMsg.style.color = "#F93"; } -}; - -HuesCore.prototype.keyHandler = function(key) { - switch (key) { - case 37: // LEFT - this.previousImage(); - break; - case 39: // RIGHT - this.nextImage(); - break; - case 38: // UP - this.nextSong(); - break; - case 40: // DOWN - this.previousSong(); - break; - case 70: // F - this.setIsFullAuto(!this.isFullAuto); - break; - case 109: // NUMPAD_SUBTRACT - case 189: // MINUS - case 173: // MINUS, legacy - this.soundManager.decreaseVolume(); - break; - case 107: // NUMPAD_ADD - case 187: // EQUAL - case 61: // EQUAL, legacy - this.soundManager.increaseVolume(); - break; - case 66: // B - this.soundManager.seek(-this.soundManager.buildLength); - break; - case 77: // M - this.soundManager.toggleMute(); - break; - case 72: // H - this.userInterface.toggleHide(); - break; - case 82: // R - this.window.selectTab("RESOURCES"); - break; - case 69: // E - this.window.selectTab("EDITOR"); - break; - case 79: // O - this.window.selectTab("OPTIONS"); - break; - case 73: // I - this.window.selectTab("INFO"); - break; - case 49: // NUMBER_1 - this.settings.set("currentUI", "retro"); - break; - case 50: // NUMBER_2 - this.settings.set("currentUI", "v4.20"); - break; - case 51: // NUMBER_3 - this.settings.set("currentUI", "modern"); - break; - case 52: // NUMBER_4 - this.settings.set("currentUI", "xmas"); - break; - case 53: // NUMBER_5 - this.settings.set("currentUI", "hlwn"); - break; - case 54: // NUMBER_6 - this.settings.set("currentUI", "mini"); - break; - case 67: // C - this.toggleImageList(); - break; - case 83: // S - this.toggleSongList(); - break; - case 87: // W - this.window.toggle(); - break; - case 78: // N - this.randomSong(); - break; - default: - return true; + + clearMessage() { + this.preloadSubMsg.textContent = ""; + this.preloadMsg.style.color = ""; } - return false; -}; - -HuesCore.prototype.error = function(message) { - console.log(message); - this.preloadSubMsg.textContent = message; - this.preloadMsg.style.color = "#F00"; -}; - -HuesCore.prototype.warning = function(message) { - console.log(message); - this.preloadSubMsg.innerHTML = message; - this.preloadMsg.style.color = "#F93"; -}; - -HuesCore.prototype.clearMessage = function() { - this.preloadSubMsg.textContent = ""; - this.preloadMsg.style.color = ""; -}; - -HuesCore.prototype.oldColours = +} + +HuesCore.oldColours = [{'c': 0x000000, 'n': 'black'}, {'c': 0x550000, 'n': 'brick'}, {'c': 0xAA0000, 'n': 'crimson'}, @@ -1111,7 +1113,7 @@ HuesCore.prototype.oldColours = {'c': 0x55FFFF, 'n': 'turquoise'}, {'c': 0xAAFFFF, 'n': 'powder'}, {'c': 0xFFFFFF, 'n': 'white'}]; -HuesCore.prototype.pastelColours = +HuesCore.pastelColours = [{'c': 0xCD4A4A, 'n': 'Mahogany'}, {'c': 0xFAE7B5, 'n': 'Banana Mania'}, {'c': 0x9F8170, 'n': 'Beaver'}, @@ -1176,7 +1178,7 @@ HuesCore.prototype.pastelColours = {'c': 0xFCE883, 'n': 'Yellow'}, {'c': 0xC5E384, 'n': 'Yellow Green'}, {'c': 0xFFB653, 'n': 'Yellow Orange'}]; -HuesCore.prototype.weedColours = +HuesCore.weedColours = [{'c': 0x00FF00, 'n': 'Green'}, {'c': 0x5A6351, 'n': 'Lizard'}, {'c': 0x636F57, 'n': 'Cactus'}, diff --git a/src/js/HuesEditor.js b/src/js/HuesEditor.js index 3e460ac..171a252 100644 --- a/src/js/HuesEditor.js +++ b/src/js/HuesEditor.js @@ -25,1379 +25,1381 @@ 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); +class HuesEditor { + constructor(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; + initUI() { + 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); - if (e.ctrlKey) { - if(key == 90) { // Z - this.undo(); - } else if(key == 89) { // Y - this.redo(); + 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; } - if(key == 90 || key == 89) { - e.preventDefault(); - return false; + 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) { + 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(); } - - // 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 += "
"; - } - // width - for(let i = 0; i < 100; i++) { - grid += " "; + + resize(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(); } - 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; + // 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 += "
"; + } + // 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; -}; + getOther(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); + onNewSong(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; } - } 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"; + onBeat(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
"); -}; - -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; + 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"; } - // 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; + + reflow(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 { - this.song.buildupName = noExt; + editor._hilight.innerHTML = "█"; } - // make empty map if needed - if(!this.getText(editor)) { - this.setText(editor, "x...o...x...o..."); + 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
"); + } + + updateInfo() { + // 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"); } - // Do we have a loop to play? if(this.song.sound) { - // Force refresh - return this.core.soundManager.playSong(this.song, true, true); + this.seekLoop.classList.remove("hues-button--disabled"); + this.loopEdit._removeBtn.classList.remove("hues-button--disabled"); } - }).then(() => { - if(newlyLinked) { - this.setIndependentBuild(false); + if(this.song.buildup) { + this.seekStart.classList.remove("hues-button--disabled"); + this.buildEdit._removeBtn.classList.remove("hues-button--disabled"); } - 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; + + 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); } - - 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(() => { + + loadAudio(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?"); }); - } 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) { + removeAudio(editor) { + if(!this.song) { + return; + } + + this.song[editor._sound] = null; + this.song[editor._rhythm] = ""; 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.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); } - 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(); + blobToArrayBuffer(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); + }); } - this.updateUndoUI(); -}; -HuesEditor.prototype.trimUndo = function() { - while(this.undoBuffer.length > 50) { - this.undoBuffer.shift(); + newSong(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.undo = function() { - this.undoRedo(this.undoBuffer, this.redoBuffer); -}; + updateIndependentBuild() { + // 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.redo = function() { - this.undoRedo(this.redoBuffer, this.undoBuffer); -}; + setIndependentBuild(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.undoRedo = function(from, to) { - if(from.length === 0 || !this.song) { - return; + batchUndo() { + if(!this.batchUndoArray) + this.batchUndoArray = []; } - // 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; + + commitUndo() { + if(this.batchUndoArray) { + this.undoBuffer.push(this.batchUndoArray); + this.trimUndo(); + this.batchUndoArray = null; + this.updateUndoUI(); } - 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"); + + pushUndo(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(); } - if(this.redoBuffer.length > 0) { - this.redoBtn.classList.remove("hues-button--disabled"); + + trimUndo() { + while(this.undoBuffer.length > 50) { + this.undoBuffer.shift(); + } } -}; - -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); + + undo() { + this.undoRedo(this.undoBuffer, this.redoBuffer); } - 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); + + redo() { + this.undoRedo(this.redoBuffer, this.undoBuffer); } -}; - -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); + + undoRedo(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(); } - 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); + + clearUndoRedo() { + this.undoBuffer = []; + this.redoBuffer = []; + this.updateUndoUI(); } -}; -HuesEditor.prototype.updateHalveDoubleButtons = function(editor) { - editor._halveBtn.className = "hues-button hues-button--disabled"; - editor._doubleBtn.className = "hues-button hues-button--disabled"; + updateUndoUI() { + 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"); + } + } - if(!editor._locked) { - let txtLen = this.getText(editor).length; + halveBeats(editor) { + let commit = false; if(!this.song.independentBuild) { + commit = true; + this.batchUndo(); + // halve them both let other = this.getOther(editor); - txtLen = Math.min(txtLen, this.getText(other).length); - } - if(txtLen > 0) { - editor._doubleBtn.className = "hues-button"; + this.song.independentBuild = true; + this.halveBeats(other); } - if(txtLen > 1) { - editor._halveBtn.className = "hues-button"; + 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.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; + + doubleBeats(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); } - }); - 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.setText(editor, this.song[editor._rhythm].replace(/(.)/g, "$1.")); + if(commit) { + this.commitUndo(); + // We set it so any rounding is padded + this.setIndependentBuild(false); } - this.song[name] = this[name].value; - if(this.song != this.core.currentSong) { - return; + } + + updateHalveDoubleButtons(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"; + } } - 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); + } + + createTextInput(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; + } + + createButton(label, parent, disabled, extraClass) { + let button = document.createElement("span"); + button.className = "hues-button"; + if(disabled) { + button.classList.add("hues-button--disabled"); } - }); - 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; + if(extraClass) { + button.className += " " + extraClass; } - 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); - }; + // 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; + } + + uiCreateInfo() { + let info = document.createElement("div"); + this.topBar.appendChild(info); + info.className = "editor__info"; - let mouseup = function(e) { - document.removeEventListener("mousemove", resizer); - document.removeEventListener("mouseup", mouseup); + 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); }; - 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 = - '
' + - '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.
' + - '
' + - '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, 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; -}; + 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.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)); -}; + uiCreateImport() { + 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.rightClick = function(editor, event) { - if(!this.linked) { - return; + uiCreateSongStat(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; } - // 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; + uiCreateEditArea() { + 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 = + '
' + + '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.
' + + '
' + + '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!'; } - 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; + + uiCreateSingleEditor(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; } - x = Math.floor((event.clientX - x) / this.hilightWidth); - y = Math.floor((event.clientY - y) / this.hilightHeight); - - return {x: x, y: y}; -}; + uiCreateControls() { + 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.textUpdated = function(editor) { - if(!this.song || !this.song[editor._sound]) { - this.reflow(editor, ""); - return; + uiCreateVisualiser() { + 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)); } - // Space at start of line is nonbreaking, get it with \u00a0 - let input = editor._beatmap.textContent.replace(/ |\u00a0/g, ""); - if(input.length === 0) { - input = "."; + + rightClick(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; } - this.setText(editor, input); -}; - -HuesEditor.prototype.getText = function(editor) { - if(!this.song || !this.song[editor._rhythm]) { - return ""; - } else { - return this.song[editor._rhythm]; + + getTextCoords(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.setText = function(editor, text, caretFromEnd) { - if(!this.song || !this.song[editor._sound]) { - this.reflow(editor, ""); - return; + textUpdated(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); } - 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); + + getText(editor) { + if(!this.song || !this.song[editor._rhythm]) { + return ""; } else { - while(text.length < editor._locked) { - text += "."; - } + return this.song[editor._rhythm]; } - // 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; + } + + setText(editor, text, caretFromEnd) { + if(!this.song || !this.song[editor._sound]) { + this.reflow(editor, ""); + return; } - 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 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); } - 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; + 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(); } - // 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(); } - // 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); - //
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; + + getCaret(editable) { + let caret = 0; + let sel = window.getSelection(); + if (sel.rangeCount) { + let range = sel.getRangeAt(0); + //
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(); - //
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; } + return 0; } -}; - -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); + setCaret(editable, caret) { + let range = document.createRange(); + let sel = window.getSelection(); + //
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.renderWave = function(buffer, length) { - if(!buffer) { - return null; + setLocked(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); } - // 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)); + + updateWaveform() { + 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); + } } - 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; + + renderWave(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; } - 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; } - 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; } - - 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); + drawWave() { + 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); } - // 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); + // 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); } - - 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 + // 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); + } } - - // 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); + + drawOneWave(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; + } + + drawWaveBar(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.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; + + confirmLeave() { + return "Unsaved beatmap - leave anyway?"; } - // Yes, this is just a bunch of strings. Simple XML, simple method. - let result = " \n"; - result += " " + this.song.title + "\n"; - if(this.song.source) { - result += " " + this.song.source + "\n"; + alert(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"); } - result += " " + this.song.rhythm + "\n"; - if(this.song.buildup) { - result += " " + this.song.buildupName + "\n"; - result += " " + this.song.buildupRhythm + "\n"; - if(this.song.independentBuild) { - result += " true\n"; + + generateXML() { + if(!this.song) { + return null; } + // Yes, this is just a bunch of strings. Simple XML, simple method. + + let 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"; + if(this.song.independentBuild) { + result += " true\n"; + } + } + result += " \n"; + return result; } - result += " \n"; - return result; -}; - -HuesEditor.prototype.saveXML = function() { - let xml = this.generateXML(); - if(!xml) { - return; - } - let result = "\n"; - result += xml; - result += "\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); + saveXML() { + let xml = this.generateXML(); + if(!xml) { + return; + } + let result = "\n"; + result += xml; + result += "\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.click(); + element.style.display = 'none'; + document.body.appendChild(element); - document.body.removeChild(element); - - window.onbeforeunload = null; -}; + element.click(); -// http://stackoverflow.com/a/30810322 -HuesEditor.prototype.copyXML = function() { - let text = this.generateXML(); - - // Clicking when disabled - if(!text) { - return; + document.body.removeChild(element); + + window.onbeforeunload = null; } - - let textArea = document.createElement("textarea"); - textArea.className = "copybox"; - textArea.value = text; + // http://stackoverflow.com/a/30810322 + copyXML() { + let text = this.generateXML(); + + // Clicking when disabled + if(!text) { + return; + } + + let textArea = document.createElement("textarea"); + textArea.className = "copybox"; - document.body.appendChild(textArea); + textArea.value = text; - textArea.select(); - - let success; + document.body.appendChild(textArea); - 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"); + 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; diff --git a/src/js/HuesInfo.js b/src/js/HuesInfo.js index bdcb149..722ac69 100644 --- a/src/js/HuesInfo.js +++ b/src/js/HuesInfo.js @@ -26,7 +26,7 @@ /* HuesInfo.js populates the INFO tab in the Hues Window. */ -let beatGlossary = [ +const beatGlossary = [ "x Vertical blur (snare)", "o Horizontal blur (bass)", "- No blur", @@ -44,7 +44,7 @@ let beatGlossary = [ "I Invert & change image" ]; -let shortcuts = [ +const shortcuts = [ "↑↓ Change song", "←→ Change image", "[N] Random song", diff --git a/src/js/HuesSettings.js b/src/js/HuesSettings.js index f2c74d5..36abe63 100644 --- a/src/js/HuesSettings.js +++ b/src/js/HuesSettings.js @@ -26,7 +26,7 @@ /* If you're modifying settings for your hues, DON'T EDIT THIS - Go to the HTML and edit the `defaults` object instead! */ -HuesSettings.prototype.defaultSettings = { +const defaultSettings = { // Location relative to root - where do the audio/zip workers live // This is required because Web Workers need an absolute path workersPath : "lib/workers/", @@ -80,7 +80,7 @@ HuesSettings.prototype.defaultSettings = { }; // Don't get saved to localStorage -HuesSettings.prototype.ephemeralSettings = [ +const ephemeralSettings = [ "load", "autoplay", "overwriteLocal", @@ -100,7 +100,7 @@ HuesSettings.prototype.ephemeralSettings = [ ]; // To dynamically build the UI like the cool guy I am -HuesSettings.prototype.settingsCategories = { +const settingsCategories = { "Functionality" : [ "autoSong", "autoSongShuffle", @@ -126,7 +126,7 @@ HuesSettings.prototype.settingsCategories = { ] }; -HuesSettings.prototype.settingsOptions = { +const settingsOptions = { smartAlign : { name : "Smart Align images", options : ["off", "on"] @@ -218,223 +218,225 @@ HuesSettings.prototype.settingsOptions = { } }; -function HuesSettings(defaults) { - this.eventListeners = { - /* callback updated() - * - * Called when settings are updated - */ - updated : [] - }; - - this.hasUI = false; - - this.settingCheckboxes = {}; - - this.textCallbacks = []; - this.visCallbacks = []; +class HuesSettings { + constructor(defaults) { + this.eventListeners = { + /* callback updated() + * + * Called when settings are updated + */ + updated : [] + }; + + this.hasUI = false; + + this.settingCheckboxes = {}; + + this.textCallbacks = []; + this.visCallbacks = []; - for(let attr in this.defaultSettings) { - if(this.defaultSettings.hasOwnProperty(attr)) { - if(defaults[attr] === undefined) { - defaults[attr] = this.defaultSettings[attr]; - } - // don't write to local if it's a temp settings - if(this.ephemeralSettings.indexOf(attr) != -1) { - continue; - } - if(defaults.overwriteLocal) { - localStorage[attr] = defaults[attr]; + for(let attr in defaultSettings) { + if(defaultSettings.hasOwnProperty(attr)) { + if(defaults[attr] === undefined) { + defaults[attr] = defaultSettings[attr]; + } + // don't write to local if it's a temp settings + if(ephemeralSettings.indexOf(attr) != -1) { + continue; + } + if(defaults.overwriteLocal) { + localStorage[attr] = defaults[attr]; + } + // populate defaults, ignoring current + if(localStorage[attr] === undefined) { + localStorage[attr] = defaults[attr]; + } } - // populate defaults, ignoring current - if(localStorage[attr] === undefined) { - localStorage[attr] = defaults[attr]; - } - } - } + } - this.defaults = defaults; -} + this.defaults = defaults; + } -HuesSettings.prototype.initUI = function(huesWin) { - let root = document.createElement("div"); - root.className = "hues-options"; - - // Don't make in every loop - let intValidator = function(self, variable) { - this.value = this.value.replace(/\D/g,''); - if(this.value === "" || this.value < 1) { - this.value = ""; - return; - } - localStorage[variable] = this.value; - self.updateConditionals(); - self.callEventListeners("updated"); - }; + initUI(huesWin) { + let root = document.createElement("div"); + root.className = "hues-options"; + + // Don't make in every loop + let intValidator = function(self, variable) { + this.value = this.value.replace(/\D/g,''); + if(this.value === "" || this.value < 1) { + this.value = ""; + return; + } + localStorage[variable] = this.value; + self.updateConditionals(); + self.callEventListeners("updated"); + }; - // To order things nicely - for(let cat in this.settingsCategories) { - if(this.settingsCategories.hasOwnProperty(cat)) { - let catContainer = document.createElement("div"); - catContainer.textContent = cat; - catContainer.className = "settings-category"; - let cats = this.settingsCategories[cat]; - for(let i = 0; i < cats.length; i++) { - let setName = cats[i]; - let setContainer = document.createElement("div"); - let setting = this.settingsOptions[setName]; - setContainer.textContent = setting.name; - setContainer.className = "settings-individual"; - let buttonContainer = document.createElement("div"); - buttonContainer.className = "settings-buttons"; - - for(let j = 0; j < setting.options.length; j++) { - let option = setting.options[j]; - if(typeof option === "string") { - let checkbox = document.createElement("input"); - // Save checkbox so we can update UI stuff - this.settingCheckboxes[setName + "-" + option] = checkbox; - checkbox.className = "settings-checkbox"; - checkbox.type = "radio"; - checkbox.value = option; - let unique = 0; - // Lets us have multiple hues on 1 page - let id = setName + "-" + option + "-"; - while(document.getElementById(id + unique)) { - unique++; - } - checkbox.name = setName + "-" + unique; - checkbox.id = id + unique; - if(localStorage[setName] == option) { - checkbox.checked = true; - } - checkbox.onclick = function(self) { - self.set(setName, this.value); - }.bind(checkbox, this); - buttonContainer.appendChild(checkbox); - // So we can style this nicely - let label = document.createElement("label"); - label.className = "settings-label"; - label.htmlFor = checkbox.id; - label.textContent = option.toUpperCase(); - buttonContainer.appendChild(label); - } else { // special option - if(option.type == "varText") { - let text = document.createElement("span"); - text.textContent = option.text(); - buttonContainer.appendChild(text); - this.textCallbacks.push({func:option.text, element:text}); - } else if(option.type == "input") { - let input = document.createElement("input"); - input.setAttribute("type", "text"); - input.className = "settings-input"; - input.value = localStorage[option.variable]; - // TODO: support more than just positive ints when the need arises - if(option.inputType == "int") { - input.oninput = intValidator.bind(input, this, option.variable); + // To order things nicely + for(let cat in settingsCategories) { + if(settingsCategories.hasOwnProperty(cat)) { + let catContainer = document.createElement("div"); + catContainer.textContent = cat; + catContainer.className = "settings-category"; + let cats = settingsCategories[cat]; + for(let i = 0; i < cats.length; i++) { + let setName = cats[i]; + let setContainer = document.createElement("div"); + let setting = settingsOptions[setName]; + setContainer.textContent = setting.name; + setContainer.className = "settings-individual"; + let buttonContainer = document.createElement("div"); + buttonContainer.className = "settings-buttons"; + + for(let j = 0; j < setting.options.length; j++) { + let option = setting.options[j]; + if(typeof option === "string") { + let checkbox = document.createElement("input"); + // Save checkbox so we can update UI stuff + this.settingCheckboxes[setName + "-" + option] = checkbox; + checkbox.className = "settings-checkbox"; + checkbox.type = "radio"; + checkbox.value = option; + let unique = 0; + // Lets us have multiple hues on 1 page + let id = setName + "-" + option + "-"; + while(document.getElementById(id + unique)) { + unique++; } - input.autofocus = false; - buttonContainer.appendChild(input); - if(option.visiblity) { - this.visCallbacks.push({func:option.visiblity, element:input}); - input.style.visibility = option.visiblity() ? "visible" : "hidden"; + checkbox.name = setName + "-" + unique; + checkbox.id = id + unique; + if(localStorage[setName] == option) { + checkbox.checked = true; + } + checkbox.onclick = function(self) { + self.set(setName, this.value); + }.bind(checkbox, this); + buttonContainer.appendChild(checkbox); + // So we can style this nicely + let label = document.createElement("label"); + label.className = "settings-label"; + label.htmlFor = checkbox.id; + label.textContent = option.toUpperCase(); + buttonContainer.appendChild(label); + } else { // special option + if(option.type == "varText") { + let text = document.createElement("span"); + text.textContent = option.text(); + buttonContainer.appendChild(text); + this.textCallbacks.push({func:option.text, element:text}); + } else if(option.type == "input") { + let input = document.createElement("input"); + input.setAttribute("type", "text"); + input.className = "settings-input"; + input.value = localStorage[option.variable]; + // TODO: support more than just positive ints when the need arises + if(option.inputType == "int") { + input.oninput = intValidator.bind(input, this, option.variable); + } + input.autofocus = false; + buttonContainer.appendChild(input); + if(option.visiblity) { + this.visCallbacks.push({func:option.visiblity, element:input}); + input.style.visibility = option.visiblity() ? "visible" : "hidden"; + } } } - } + } + setContainer.appendChild(buttonContainer); + catContainer.appendChild(setContainer); } - setContainer.appendChild(buttonContainer); - catContainer.appendChild(setContainer); + root.appendChild(catContainer); } - root.appendChild(catContainer); } + huesWin.addTab("OPTIONS", root); + this.hasUI = true; } - huesWin.addTab("OPTIONS", root); - this.hasUI = true; -}; -HuesSettings.prototype.get = function(setting) { - if(this.defaults.hasOwnProperty(setting)) { - if(this.ephemeralSettings.indexOf(setting) != -1) { - return this.defaults[setting]; + get(setting) { + if(this.defaults.hasOwnProperty(setting)) { + if(ephemeralSettings.indexOf(setting) != -1) { + return this.defaults[setting]; + } else { + return localStorage[setting]; + } } else { - return localStorage[setting]; + console.log("WARNING: Attempted to fetch invalid setting:", setting); + return null; } - } else { - console.log("WARNING: Attempted to fetch invalid setting:", setting); - return null; } -}; -// Set a named index to its named value, returns false if name doesn't exist -HuesSettings.prototype.set = function(setting, value) { - value = value.toLowerCase(); - let opt = this.settingsOptions[setting]; - if(!opt || opt.options.indexOf(value) == -1) { - console.log(value, "is not a valid value for", setting); - return false; + // Set a named index to its named value, returns false if name doesn't exist + set(setting, value) { + value = value.toLowerCase(); + let opt = settingsOptions[setting]; + if(!opt || opt.options.indexOf(value) == -1) { + console.log(value, "is not a valid value for", setting); + return false; + } + // for updating the UI selection + try { + this.settingCheckboxes[setting + "-" + value].checked = true; + } catch(e) {} + localStorage[setting] = value; + this.updateConditionals(); + this.callEventListeners("updated"); + return true; } - // for updating the UI selection - try { - this.settingCheckboxes[setting + "-" + value].checked = true; - } catch(e) {} - localStorage[setting] = value; - this.updateConditionals(); - this.callEventListeners("updated"); - return true; -}; -HuesSettings.prototype.updateConditionals = function() { - // update any conditionally formatted settings text - for(let i = 0; i < this.textCallbacks.length; i++) { - let text = this.textCallbacks[i]; - text.element.textContent = text.func(); - } - for(let i = 0; i < this.visCallbacks.length; i++) { - let callback = this.visCallbacks[i]; - callback.element.style.visibility = callback.func() ? "visible" : "hidden"; + updateConditionals() { + // update any conditionally formatted settings text + for(let i = 0; i < this.textCallbacks.length; i++) { + let text = this.textCallbacks[i]; + text.element.textContent = text.func(); + } + for(let i = 0; i < this.visCallbacks.length; i++) { + let callback = this.visCallbacks[i]; + callback.element.style.visibility = callback.func() ? "visible" : "hidden"; + } } -}; -// Note: This is not defaults as per defaultSettings, but those merged with -// the defaults given in the initialiser -HuesSettings.prototype.setDefaults = function() { - for(let attr in this.defaults) { - if(this.defaults.hasOwnProperty(attr)) { - if(this.ephemeralSettings.indexOf(attr) != -1) { - continue; + // Note: This is not defaults as per defaultSettings, but those merged with + // the defaults given in the initialiser + setDefaults() { + for(let attr in this.defaults) { + if(this.defaults.hasOwnProperty(attr)) { + if(ephemeralSettings.indexOf(attr) != -1) { + continue; + } + localStorage[attr] = this.defaults[attr]; } - localStorage[attr] = this.defaults[attr]; } } -}; -HuesSettings.prototype.callEventListeners = function(ev) { - let args = Array.prototype.slice.call(arguments, 1); - this.eventListeners[ev].forEach(function(callback) { - callback.apply(null, args); - }); -}; + callEventListeners(ev) { + let args = Array.prototype.slice.call(arguments, 1); + this.eventListeners[ev].forEach(function(callback) { + callback.apply(null, args); + }); + } -HuesSettings.prototype.addEventListener = function(ev, callback) { - ev = ev.toLowerCase(); - if (typeof(this.eventListeners[ev]) !== "undefined") { - this.eventListeners[ev].push(callback); - } else { - throw Error("Unknown event: " + ev); + addEventListener(ev, callback) { + ev = ev.toLowerCase(); + if (typeof(this.eventListeners[ev]) !== "undefined") { + this.eventListeners[ev].push(callback); + } else { + throw Error("Unknown event: " + ev); + } } -}; -HuesSettings.prototype.removeEventListener = function(ev, callback) { - ev = ev.toLowerCase(); - if (typeof(this.eventListeners[ev]) !== "undefined") { - this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { - return (a !== callback); - }); - } else { - throw Error("Unknown event: " + ev); + removeEventListener(ev, callback) { + ev = ev.toLowerCase(); + if (typeof(this.eventListeners[ev]) !== "undefined") { + this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { + return (a !== callback); + }); + } else { + throw Error("Unknown event: " + ev); + } } -}; +} window.HuesSettings = HuesSettings; diff --git a/src/js/HuesUI.js b/src/js/HuesUI.js index 8197cd3..1fd6bed 100644 --- a/src/js/HuesUI.js +++ b/src/js/HuesUI.js @@ -27,1185 +27,1160 @@ to put all your own elements under, but make a div underneath so it can be entirely hidden. */ -function HuesUI(parent, name) { - if(!parent) { - return; +class HuesUI { + + constructor(parent, name) { + if(!parent) { + return; + } + this.root = document.createElement("div"); + this.root.className = name ? name : this.constructor.name; + parent.appendChild(this.root); + this.root.style.display = "none"; + + this.core = null; + + this.imageName = null; + this.imageLink = null; + + this.songName = null; + this.songLink = null; + + this.hueName = null; + + this.imagePrev = null; + this.imageNext = null; + this.songPrev = null; + this.songNext = null; + + this.beatCount = null; + this.timer = null; + this.xBlur = null; + this.yBlur = null; + + this.settingsToggle = null; + this.hideToggle = null; + + // To deregister on UI hide we need to keep track of these + // Each callback is { name : "callbackname", func : function } + // Add using this.addCoreCallback + this.callbacks = []; + + // Put this near the links to song/image lists/ Bottom right alignment + this.listContainer = null; + // Must be dynamic width, 64 pixels high. Will be filled with visualiser + this.visualiserContainer = null; + + this.hidden = false; + + this.initUI(); } - this.root = document.createElement("div"); - this.root.className = name ? name : this.constructor.name; - parent.appendChild(this.root); - this.root.style.display = "none"; - this.core = null; + addCoreCallback(name, func) { + this.callbacks.push({name : name, func : func}); + } - this.imageName = null; - this.imageLink = null; + initUI() { + // Major info, image, song names + let imageName = document.createElement("div"); + this.imageName = imageName; + + this.imageLink = document.createElement("a"); + this.imageLink.target = "_blank"; + this.imageName.appendChild(this.imageLink); + + let songName = document.createElement("div"); + this.songName = songName; + + this.songLink = document.createElement("a"); + this.songLink.target = "_blank"; + this.songName.appendChild(this.songLink); + + let hueName = document.createElement("div"); + this.hueName = hueName; + + // Prev/next controls + let imagePrev = document.createElement("div"); + imagePrev.textContent = "<"; + imagePrev.onclick = () => {this.core.previousImage();}; + this.imagePrev = imagePrev; + let imageNext = document.createElement("div"); + imageNext.textContent = ">"; + imageNext.onclick = () =>{this.core.nextImage();}; + this.imageNext = imageNext; + let songPrev = document.createElement("div"); + songPrev.textContent = "<"; + this.songPrev = songPrev; + songPrev.onclick = () =>{this.core.previousSong();}; + let songNext = document.createElement("div"); + songNext.textContent = ">"; + songNext.onclick = () =>{this.core.nextSong();}; + this.songNext = songNext; + + let songList = document.createElement("div"); + songList.textContent = "SONGS"; + songList.onclick = () =>{this.core.toggleSongList();}; + this.songList = songList; + let imageList = document.createElement("div"); + imageList.textContent = "IMAGES"; + imageList.onclick = () =>{this.core.toggleImageList();}; + this.imageList = imageList; + + // Beat timer, x and y blur, millis timer + this.timer = document.createElement("div"); + this.timer.textContent = "T=$0x00000"; + + this.beatCount = document.createElement("div"); + this.beatCount.textContent = "B=$0x0000"; + + this.xBlur = document.createElement("div"); + this.xBlur.textContent = "X=$0x00"; + + this.yBlur = document.createElement("div"); + this.yBlur.textContent = "Y=$0x00"; + + // Config stuff + this.settingsToggle = document.createElement("div"); + this.settingsToggle.innerHTML = ''; // COG + this.settingsToggle.className = 'hues-icon'; + this.settingsToggle.onclick = () => { + this.core.window.toggle(); + }; + + this.hideToggle = document.createElement("div"); + this.hideToggle.innerHTML = "▼"; + this.hideToggle.onclick = () => { + this.toggleHide(); + }; + + this.listContainer = document.createElement("div"); + this.visualiserContainer = document.createElement("div"); + + this.addCoreCallback("newsong", this.newSong.bind(this)); + this.addCoreCallback("newimage", this.newImage.bind(this)); + this.addCoreCallback("newcolour", this.newColour.bind(this)); + this.addCoreCallback("blurupdate", this.blurUpdated.bind(this)); + this.addCoreCallback("time", this.updateTime.bind(this)); + this.addCoreCallback("invert", this.invert.bind(this)); + this.resizeHandler = this.resize.bind(this); + } - this.songName = null; - this.songLink = null; + connectCore(core) { + this.core = core; + this.root.style.display = "block"; + if(core.resourceManager.hasUI) { + this.listContainer.appendChild(core.resourceManager.listView); + } + this.visualiserContainer.appendChild(this.core.visualiser); - this.hueName = null; + this.callbacks.forEach(function(callback) { + core.addEventListener(callback.name, callback.func); + }); + window.addEventListener('resize', this.resizeHandler); + this.resizeHandler(); + } - this.imagePrev = null; - this.imageNext = null; - this.songPrev = null; - this.songNext = null; + disconnect() { + this.callbacks.forEach(callback => { + this.core.removeEventListener(callback.name, callback.func); + }); + this.core = null; + this.root.style.display = "none"; + while (this.listContainer.firstElementChild) { + this.listContainer.removeChild(this.listContainer.firstElementChild); + } + while (this.visualiserContainer.firstElementChild) { + this.visualiserContainer.removeChild(this.visualiserContainer.firstElementChild); + } + window.removeEventListener('resize', this.resizeHandler); + } - this.beatCount = null; - this.timer = null; - this.xBlur = null; - this.yBlur = null; + // ONLY FOR CHANGING UI, NOT FOR "HIDE" FEATURE + show() { + this.root.style.display = "block"; + } - this.settingsToggle = null; - this.hideToggle = null; - - // To deregister on UI hide we need to keep track of these - // Each callback is { name : "callbackname", func : function } - // Add using this.addCoreCallback - this.callbacks = []; + // ONLY FOR CHANGING UI, NOT FOR "HIDE" FEATURE + hide() { + this.root.style.display = "none"; + } - // Put this near the links to song/image lists/ Bottom right alignment - this.listContainer = null; - // Must be dynamic width, 64 pixels high. Will be filled with visualiser - this.visualiserContainer = null; + toggleHide() { + this.hidden = !this.hidden; + if(this.hidden) { + this.root.classList.add("hues-ui--hidden"); + } else { + this.root.classList.remove("hues-ui--hidden"); + } + } - this.hidden = false; + resize() {} + updateVolume(vol) {} - this.initUI(); -} + newSong(song) { + if(!song) { + return; + } + + this.songLink.textContent = song.title.toUpperCase(); + this.songLink.href = song.source; + } + + newImage(image) { + if(!image) { + return; + } + + let name = image.fullname ? image.fullname : image.name; + + this.imageLink.textContent = name.toUpperCase(); + this.imageLink.href = image.source ? image.source : ""; + } + + newColour(colour) { + this.hueName.textContent = colour.n.toUpperCase(); + } + + blurUpdated(x, y) { + x = Math.floor(x * 0xFF); + y = Math.floor(y * 0xFF); + this.xBlur.textContent = "X=" + this.intToHex(x, 2); + this.yBlur.textContent = "Y=" + this.intToHex(y, 2); + } -HuesUI.prototype.addCoreCallback = function(name, func) { - this.callbacks.push({name : name, func : func}); -}; - -HuesUI.prototype.initUI = function() { - // Major info, image, song names - let imageName = document.createElement("div"); - this.imageName = imageName; - - this.imageLink = document.createElement("a"); - this.imageLink.target = "_blank"; - this.imageName.appendChild(this.imageLink); - - let songName = document.createElement("div"); - this.songName = songName; - - this.songLink = document.createElement("a"); - this.songLink.target = "_blank"; - this.songName.appendChild(this.songLink); - - let hueName = document.createElement("div"); - this.hueName = hueName; - - // Prev/next controls - let imagePrev = document.createElement("div"); - imagePrev.textContent = "<"; - imagePrev.onclick = () => {this.core.previousImage();}; - this.imagePrev = imagePrev; - let imageNext = document.createElement("div"); - imageNext.textContent = ">"; - imageNext.onclick = () =>{this.core.nextImage();}; - this.imageNext = imageNext; - let songPrev = document.createElement("div"); - songPrev.textContent = "<"; - this.songPrev = songPrev; - songPrev.onclick = () =>{this.core.previousSong();}; - let songNext = document.createElement("div"); - songNext.textContent = ">"; - songNext.onclick = () =>{this.core.nextSong();}; - this.songNext = songNext; - - let songList = document.createElement("div"); - songList.textContent = "SONGS"; - songList.onclick = () =>{this.core.toggleSongList();}; - this.songList = songList; - let imageList = document.createElement("div"); - imageList.textContent = "IMAGES"; - imageList.onclick = () =>{this.core.toggleImageList();}; - this.imageList = imageList; - - // Beat timer, x and y blur, millis timer - this.timer = document.createElement("div"); - this.timer.textContent = "T=$0x00000"; - - this.beatCount = document.createElement("div"); - this.beatCount.textContent = "B=$0x0000"; - - this.xBlur = document.createElement("div"); - this.xBlur.textContent = "X=$0x00"; - - this.yBlur = document.createElement("div"); - this.yBlur.textContent = "Y=$0x00"; - - // Config stuff - this.settingsToggle = document.createElement("div"); - this.settingsToggle.innerHTML = ''; // COG - this.settingsToggle.className = 'hues-icon'; - this.settingsToggle.onclick = () => { - this.core.window.toggle(); - }; - - this.hideToggle = document.createElement("div"); - this.hideToggle.innerHTML = "▼"; - this.hideToggle.onclick = () => { - this.toggleHide(); - }; - - this.listContainer = document.createElement("div"); - this.visualiserContainer = document.createElement("div"); - - this.addCoreCallback("newsong", this.newSong.bind(this)); - this.addCoreCallback("newimage", this.newImage.bind(this)); - this.addCoreCallback("newcolour", this.newColour.bind(this)); - this.addCoreCallback("blurupdate", this.blurUpdated.bind(this)); - this.addCoreCallback("time", this.updateTime.bind(this)); - this.addCoreCallback("invert", this.invert.bind(this)); - this.resizeHandler = this.resize.bind(this); -}; - -HuesUI.prototype.connectCore = function(core) { - this.core = core; - this.root.style.display = "block"; - if(core.resourceManager.hasUI) { - this.listContainer.appendChild(core.resourceManager.listView); - } - this.visualiserContainer.appendChild(this.core.visualiser); - - this.callbacks.forEach(function(callback) { - core.addEventListener(callback.name, callback.func); - }); - window.addEventListener('resize', this.resizeHandler); - this.resizeHandler(); -}; - -HuesUI.prototype.disconnect = function() { - this.callbacks.forEach(callback => { - this.core.removeEventListener(callback.name, callback.func); - }); - this.core = null; - this.root.style.display = "none"; - while (this.listContainer.firstElementChild) { - this.listContainer.removeChild(this.listContainer.firstElementChild); - } - while (this.visualiserContainer.firstElementChild) { - this.visualiserContainer.removeChild(this.visualiserContainer.firstElementChild); - } - window.removeEventListener('resize', this.resizeHandler); -}; - -// ONLY FOR CHANGING UI, NOT FOR "HIDE" FEATURE -HuesUI.prototype.show = function() { - this.root.style.display = "block"; -}; - -// ONLY FOR CHANGING UI, NOT FOR "HIDE" FEATURE -HuesUI.prototype.hide = function() { - this.root.style.display = "none"; -}; - -HuesUI.prototype.toggleHide = function() { - this.hidden = !this.hidden; - if(this.hidden) { - this.root.classList.add("hues-ui--hidden"); - } else { - this.root.classList.remove("hues-ui--hidden"); - } -}; - -HuesUI.prototype.resize = function() {}; -HuesUI.prototype.updateVolume = function(vol) {}; - -HuesUI.prototype.newSong = function(song) { - if(!song) { - return; - } - - this.songLink.textContent = song.title.toUpperCase(); - this.songLink.href = song.source; -}; - -HuesUI.prototype.newImage = function(image) { - if(!image) { - return; - } - - let name = image.fullname ? image.fullname : image.name; - - this.imageLink.textContent = name.toUpperCase(); - this.imageLink.href = image.source ? image.source : ""; -}; - -HuesUI.prototype.newColour = function(colour) { - this.hueName.textContent = colour.n.toUpperCase(); -}; - -HuesUI.prototype.blurUpdated = function(x, y) { - x = Math.floor(x * 0xFF); - y = Math.floor(y * 0xFF); - this.xBlur.textContent = "X=" + this.intToHex(x, 2); - this.yBlur.textContent = "Y=" + this.intToHex(y, 2); -}; - -HuesUI.prototype.updateTime = function(time) { - time = Math.floor(time * 1000); - this.timer.textContent = "T=" + this.intToHex(time, 5); -}; - -HuesUI.prototype.intToHex = function(num, pad) { - let str = Math.abs(num).toString(16); - while (str.length < pad) - str = "0" + str; - let prefix = num < 0 ? "-" : "$"; - return prefix + "0x" + str; -}; - -HuesUI.prototype.invert = function(invert) { - if (invert) { - this.root.classList.add("inverted"); - } else { - this.root.classList.remove("inverted"); - } -}; + updateTime(time) { + time = Math.floor(time * 1000); + this.timer.textContent = "T=" + this.intToHex(time, 5); + } + + intToHex(num, pad) { + let str = Math.abs(num).toString(16); + while (str.length < pad) + str = "0" + str; + let prefix = num < 0 ? "-" : "$"; + return prefix + "0x" + str; + } + + invert(invert) { + if (invert) { + this.root.classList.add("inverted"); + } else { + this.root.classList.remove("inverted"); + } + } +} /* Individual UIs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -function RetroUI(parent, name) { - this.container = null; - this.mode = null; - this.beatBar = null; - this.colourIndex = null; - this.version = null; - this.imageModeAuto = null; - this.imageModeAuto = null; - this.subControls = null; - - HuesUI.call(this, parent, name ? name : "RetroUI"); -} +class RetroUI extends HuesUI { + constructor(parent, name) { + super(parent, name ? name : "RetroUI"); + } -RetroUI.prototype = Object.create(HuesUI.prototype); -RetroUI.prototype.constructor = RetroUI; - -RetroUI.prototype.initUI = function() { - HuesUI.prototype.initUI.call(this); - - let container = document.createElement("div"); - container.className = "hues-r-container"; - this.root.appendChild(container); - this.container = container; - - this.mode = document.createElement("div"); - container.appendChild(this.mode); - container.appendChild(this.imageName); - container.appendChild(this.timer); - container.appendChild(this.beatCount); - container.appendChild(this.xBlur); - container.appendChild(this.yBlur); - - this.colourIndex = document.createElement("div"); - this.colourIndex.textContent = "C=$0x00"; - container.appendChild(this.colourIndex); - - this.version = document.createElement("div"); - container.appendChild(this.version); - - container.appendChild(this.hueName); - container.appendChild(this.songName); - - this.beatBar = document.createElement("div"); - container.appendChild(this.beatBar); - - this.controls = document.createElement("div"); - this.controls.className = "hues-r-controls"; - - let imageMode = document.createElement("div"); - this.imageModeManual = document.createElement("div"); - this.imageModeManual.textContent = "NORMAL"; - this.imageModeManual.onclick = () => { - this.core.setIsFullAuto(false); - }; - this.imageModeManual.className = "hues-r-manualmode hues-r-button"; - this.imageModeAuto = document.createElement("div"); - this.imageModeAuto.textContent = "FULL AUTO"; - this.imageModeAuto.onclick = () => { - this.core.setIsFullAuto(true); - }; - this.imageModeAuto.className = "hues-r-automode hues-r-button"; - imageMode.appendChild(this.imageModeManual); - imageMode.appendChild(this.imageModeAuto); - - this.imagePrev.className = "hues-r-button"; - this.imageNext.className = "hues-r-button"; - this.songPrev.className = "hues-r-button"; - this.songNext.className = "hues-r-button"; - this.controls.appendChild(this.imagePrev); - this.controls.appendChild(imageMode); - this.controls.appendChild(this.imageNext); - - this.songList.className = "hues-r-songs hues-r-button"; - this.controls.appendChild(this.songPrev); - this.controls.appendChild(this.songList); - this.controls.appendChild(this.songNext); - - this.root.appendChild(this.controls); - - let subControl = document.createElement("div"); - subControl.className = "hues-r-subcontrols"; - subControl.appendChild(this.settingsToggle); - this.imageList.textContent = "C"; - subControl.appendChild(this.imageList); - subControl.appendChild(this.hideToggle); - this.subControls = subControl; - - this.root.appendChild(subControl); - - this.hideRestore = document.createElement("div"); - this.hideRestore.className = "hues-r-hiderestore"; - this.hideRestore.innerHTML = "▲"; - this.hideRestore.onclick = () => { - this.toggleHide(); - }; - this.root.appendChild(this.hideRestore); - - this.listContainer.className = "hues-r-listcontainer"; - this.root.appendChild(this.listContainer); - - this.visualiserContainer.className = "hues-r-visualisercontainer"; - this.root.appendChild(this.visualiserContainer); - - this.addCoreCallback("beat", this.beat.bind(this)); - this.addCoreCallback("newmode", this.newMode.bind(this)); -}; - -RetroUI.prototype.toggleHide = function() { - this.hidden = !this.hidden; - if(this.hidden) { - this.subControls.classList.add("hues-ui--hidden"); - this.controls.classList.add("hues-ui--hidden"); - this.container.classList.add("hues-ui--hidden"); - this.hideRestore.classList.add("hues-ui--hidden"); - } else { - this.subControls.classList.remove("hues-ui--hidden"); - this.controls.classList.remove("hues-ui--hidden"); - this.container.classList.remove("hues-ui--hidden"); - this.hideRestore.classList.remove("hues-ui--hidden"); + initUI() { + super.initUI(); + + let container = document.createElement("div"); + container.className = "hues-r-container"; + this.root.appendChild(container); + this.container = container; + + this.mode = document.createElement("div"); + container.appendChild(this.mode); + container.appendChild(this.imageName); + container.appendChild(this.timer); + container.appendChild(this.beatCount); + container.appendChild(this.xBlur); + container.appendChild(this.yBlur); + + this.colourIndex = document.createElement("div"); + this.colourIndex.textContent = "C=$0x00"; + container.appendChild(this.colourIndex); + + this.version = document.createElement("div"); + container.appendChild(this.version); + + container.appendChild(this.hueName); + container.appendChild(this.songName); + + this.beatBar = document.createElement("div"); + container.appendChild(this.beatBar); + + this.controls = document.createElement("div"); + this.controls.className = "hues-r-controls"; + + let imageMode = document.createElement("div"); + this.imageModeManual = document.createElement("div"); + this.imageModeManual.textContent = "NORMAL"; + this.imageModeManual.onclick = () => { + this.core.setIsFullAuto(false); + }; + this.imageModeManual.className = "hues-r-manualmode hues-r-button"; + this.imageModeAuto = document.createElement("div"); + this.imageModeAuto.textContent = "FULL AUTO"; + this.imageModeAuto.onclick = () => { + this.core.setIsFullAuto(true); + }; + this.imageModeAuto.className = "hues-r-automode hues-r-button"; + imageMode.appendChild(this.imageModeManual); + imageMode.appendChild(this.imageModeAuto); + + this.imagePrev.className = "hues-r-button"; + this.imageNext.className = "hues-r-button"; + this.songPrev.className = "hues-r-button"; + this.songNext.className = "hues-r-button"; + this.controls.appendChild(this.imagePrev); + this.controls.appendChild(imageMode); + this.controls.appendChild(this.imageNext); + + this.songList.className = "hues-r-songs hues-r-button"; + this.controls.appendChild(this.songPrev); + this.controls.appendChild(this.songList); + this.controls.appendChild(this.songNext); + + this.root.appendChild(this.controls); + + let subControl = document.createElement("div"); + subControl.className = "hues-r-subcontrols"; + subControl.appendChild(this.settingsToggle); + this.imageList.textContent = "C"; + subControl.appendChild(this.imageList); + subControl.appendChild(this.hideToggle); + this.subControls = subControl; + + this.root.appendChild(subControl); + + this.hideRestore = document.createElement("div"); + this.hideRestore.className = "hues-r-hiderestore"; + this.hideRestore.innerHTML = "▲"; + this.hideRestore.onclick = () => { + this.toggleHide(); + }; + this.root.appendChild(this.hideRestore); + + this.listContainer.className = "hues-r-listcontainer"; + this.root.appendChild(this.listContainer); + + this.visualiserContainer.className = "hues-r-visualisercontainer"; + this.root.appendChild(this.visualiserContainer); + + this.addCoreCallback("beat", this.beat.bind(this)); + this.addCoreCallback("newmode", this.newMode.bind(this)); } -}; -RetroUI.prototype.connectCore = function(core) { - HuesUI.prototype.connectCore.call(this, core); + toggleHide() { + this.hidden = !this.hidden; + if(this.hidden) { + this.subControls.classList.add("hues-ui--hidden"); + this.controls.classList.add("hues-ui--hidden"); + this.container.classList.add("hues-ui--hidden"); + this.hideRestore.classList.add("hues-ui--hidden"); + } else { + this.subControls.classList.remove("hues-ui--hidden"); + this.controls.classList.remove("hues-ui--hidden"); + this.container.classList.remove("hues-ui--hidden"); + this.hideRestore.classList.remove("hues-ui--hidden"); + } + } - this.version.textContent = "V=$" + core.versionHex; -}; + connectCore(core) { + super.connectCore(core); -RetroUI.prototype.newMode = function(isAuto) { - this.mode.textContent = "M=" + (isAuto ? "FULL AUTO" : "NORMAL"); -}; + this.version.textContent = "V=$" + core.versionHex; + } -RetroUI.prototype.newImage = function(image) { - if(!image) { - return; + newMode(isAuto) { + this.mode.textContent = "M=" + (isAuto ? "FULL AUTO" : "NORMAL"); } - this.imageLink.textContent = "I=" + image.name.toUpperCase(); - this.imageLink.href = image.source; -}; + newImage(image) { + if(!image) { + return; + } -RetroUI.prototype.newColour = function(colour) { - HuesUI.prototype.newColour.call(this, colour); + this.imageLink.textContent = "I=" + image.name.toUpperCase(); + this.imageLink.href = image.source; + } - this.colourIndex.textContent = "C=" + this.intToHex(this.core.colourIndex, 2); -}; + newColour(colour) { + super.newColour(colour); -RetroUI.prototype.beat = function(beats, index) { - let rest = beats.slice(1); - this.beatBar.textContent = ">>" + rest; - this.beatCount.textContent = "B=" + this.intToHex(index, 4); -}; + this.colourIndex.textContent = "C=" + this.intToHex(this.core.colourIndex, 2); + } -RetroUI.prototype.resize = function() { - this.core.visualiser.width = this.visualiserContainer.offsetWidth; - this.core.resizeVisualiser(); -}; + beat(beats, index) { + let rest = beats.slice(1); + this.beatBar.textContent = ">>" + rest; + this.beatCount.textContent = "B=" + this.intToHex(index, 4); + } -function MinimalUI(parent, name) { - RetroUI.call(this, parent, name ? name : "MinimalUI"); + resize() { + this.core.visualiser.width = this.visualiserContainer.offsetWidth; + this.core.resizeVisualiser(); + } } -MinimalUI.prototype = Object.create(RetroUI.prototype); -MinimalUI.prototype.constructor = MinimalUI; +class MinimalUI extends RetroUI { + constructor(parent, name) { + super(parent, name ? name : "MinimalUI"); + } -MinimalUI.prototype.initUI = function() { - RetroUI.prototype.initUI.call(this); - - this.root.removeChild(this.controls); - this.root.removeChild(this.subControls); - this.container.removeChild(this.beatBar); - this.container.innerHTML = ""; - this.container.appendChild(this.beatBar); -}; - -function WeedUI(parent, name) { - RetroUI.call(this, parent, name ? name : "WeedUI"); - - this.xVariance = 10; - this.yVariance = 20; + initUI() { + super.initUI(); + + this.root.removeChild(this.controls); + this.root.removeChild(this.subControls); + this.container.removeChild(this.beatBar); + this.container.innerHTML = ""; + this.container.appendChild(this.beatBar); + } } -WeedUI.prototype = Object.create(RetroUI.prototype); -WeedUI.prototype.constructor = WeedUI; +class WeedUI extends RetroUI { + constructor(parent, name) { + super(parent, name ? name : "WeedUI"); -WeedUI.prototype.initUI = function() { - RetroUI.prototype.initUI.call(this); + this.xVariance = 10; + this.yVariance = 20; + } - this.container.removeChild(this.beatBar); + initUI() { + super.initUI(); - this.controls.className = "hues-w-controls"; - this.subControls.className = "hues-w-subcontrols"; + this.container.removeChild(this.beatBar); - let beatBar = document.createElement("div"); - beatBar.className = "hues-w-beatbar"; - this.root.appendChild(beatBar); - this.beatBar = beatBar; + this.controls.className = "hues-w-controls"; + this.subControls.className = "hues-w-subcontrols"; - let beatLeft = document.createElement("div"); - beatLeft.className = "hues-w-beatleft"; - beatBar.appendChild(beatLeft); - this.beatLeft = beatLeft; + let beatBar = document.createElement("div"); + beatBar.className = "hues-w-beatbar"; + this.root.appendChild(beatBar); + this.beatBar = beatBar; - let beatRight = document.createElement("div"); - beatRight.className = "hues-w-beatright"; - beatBar.appendChild(beatRight); - this.beatRight = beatRight; + let beatLeft = document.createElement("div"); + beatLeft.className = "hues-w-beatleft"; + beatBar.appendChild(beatLeft); + this.beatLeft = beatLeft; - this.imageModeManual.textContent = "ONE"; - this.imageModeAuto.textContent = "MANY"; - - this.visualiserContainer.className += " hues-w-visualisercontainer"; -}; - -WeedUI.prototype.toggleHide = function() { - RetroUI.prototype.toggleHide.call(this); - if(this.hidden) { - this.beatBar.classList.add("hues-ui--hidden"); - } else { - this.beatBar.classList.remove("hues-ui--hidden"); + let beatRight = document.createElement("div"); + beatRight.className = "hues-w-beatright"; + beatBar.appendChild(beatRight); + this.beatRight = beatRight; + + this.imageModeManual.textContent = "ONE"; + this.imageModeAuto.textContent = "MANY"; + + this.visualiserContainer.className += " hues-w-visualisercontainer"; } -}; -WeedUI.prototype.beat = function(beats, index) { - let rest = beats.slice(1); + toggleHide() { + super.toggleHide(this); + if(this.hidden) { + this.beatBar.classList.add("hues-ui--hidden"); + } else { + this.beatBar.classList.remove("hues-ui--hidden"); + } + } - this.beatLeft.textContent = rest; - this.beatRight.textContent = rest; - - this.beatCount.textContent = "B=" + this.intToHex(index, 4); + beat(beats, index) { + let rest = beats.slice(1); + + this.beatLeft.textContent = rest; + this.beatRight.textContent = rest; + + this.beatCount.textContent = "B=" + this.intToHex(index, 4); + + if(["x", "o", "X", "O"].indexOf(beats[0]) != -1) { + let beatCenter = document.createElement("div"); + beatCenter.className = "hues-w-beataccent"; + let rot = this.round10(15 - Math.random() * 30); + let x = this.round10(- this.xVariance / 2 + Math.random() * this.xVariance); + let y = this.round10(30 - this.yVariance / 2 + Math.random() * this.yVariance); + let transform = "rotate(" + rot + "deg) translate(" + x + "px, " + y + "px)"; + beatCenter.style.MozTransform = transform; + beatCenter.style.webkitTransform = transform; + beatCenter.style.transform = transform; + beatCenter.textContent = beats[0].toUpperCase(); + this.root.appendChild(beatCenter); + window.setTimeout(this.removeBeat.bind(this, beatCenter), 1500); + } + } + + round10(num) { + return Math.round(num * 10) / 10; + } + + removeBeat(element) { + this.root.removeChild(element); + } +} + +class ModernUI extends HuesUI { + constructor(parent, name) { + super(parent, name ? name : "ModernUI"); + + this.textSize_normal = 0; + this.textSize_small = 0; + this.songLink_size = 0; + this.imageLink_size = 0; + + this.currentBeat = "."; + + this.hidden = 0; // we have a 3 stage hide + } + + initUI() { + super.initUI(); + + this.imageName.className = "hues-m-imagename"; + this.songName.className = "hues-m-songtitle"; + + let controls = document.createElement("div"); + controls.className = "hues-m-controls"; + this.root.appendChild(controls); + this.controls = controls; + + controls.appendChild(this.imageName); + controls.appendChild(this.songName); + + let leftBox = document.createElement("div"); + leftBox.className = "hues-m-leftbox"; + controls.appendChild(leftBox); + this.leftBox = leftBox; + + this.hueName.className = "hues-m-huename"; + leftBox.appendChild(this.hueName); + + let volCluster = document.createElement("div"); + volCluster.className = "hues-m-vol-cluster"; + leftBox.appendChild(volCluster); + + this.settingsToggle.className += " hues-m-cog"; + volCluster.appendChild(this.settingsToggle); + + this.hideToggle.className = "hues-m-hide"; + volCluster.appendChild(this.hideToggle); + + let volBar = document.createElement("div"); + volBar.className = "hues-m-vol-bar"; + this.volBar = volBar; + volCluster.appendChild(volBar); + + let label = document.createElement("div"); + label.textContent = "VOL"; + label.className = "hues-m-vol-label"; + label.onclick = () => { + this.core.soundManager.toggleMute(); + }; + volBar.appendChild(label); + this.volLabel = label; + + this.infoToggle = document.createElement("div"); + this.infoToggle.innerHTML = '?'; + this.infoToggle.className = "hues-m-question"; + this.infoToggle.onclick = () => { + this.core.window.selectTab("INFO"); + }; + volCluster.appendChild(this.infoToggle); + + let input = document.createElement("input"); + input.type = "range"; + input.min = 0; + input.max = 1; + input.step = 0.1; + volBar.appendChild(input); + this.volInput = input; + input.oninput = () => { + this.core.soundManager.setVolume(parseFloat(input.value)); + }; + + let rightBox = document.createElement("div"); + rightBox.className = "hues-m-rightbox"; + controls.appendChild(rightBox); + this.rightBox = rightBox; + + //Song/image controls + let songs = document.createElement("div"); + songs.className = "hues-m-controlblock"; + this.songBlock = songs; + this.songList.className = "hues-m-songbutton"; + + let songControls = document.createElement("div"); + songControls.className = "hues-m-controlbuttons"; + this.songPrev.className = "hues-m-prevbutton"; + this.songNext.className = "hues-m-nextbutton"; + this.songShuffle = document.createElement("div"); + this.songShuffle.innerHTML = ''; // SHUFFLE + this.songShuffle.className = "hues-m-actbutton hues-icon"; + this.songShuffle.onclick = () => {this.core.randomSong();}; + songs.appendChild(this.songList); + songControls.appendChild(this.songPrev); + songControls.appendChild(this.songShuffle); + songControls.appendChild(this.songNext); + songs.appendChild(songControls); + rightBox.appendChild(songs); + + let images = document.createElement("div"); + images.className = "hues-m-controlblock"; + this.imageList.className = "hues-m-songbutton"; + this.imageBlock = images; + + let imageControls = document.createElement("div"); + imageControls.className = "hues-m-controlbuttons"; + + this.imageMode = document.createElement("div"); + this.imageMode.innerHTML = ""; // PLAY + this.imageMode.className = "hues-m-actbutton hues-icon"; + this.imageMode.onclick = () => {this.core.toggleFullAuto();}; + this.imagePrev.className = "hues-m-prevbutton"; + this.imageNext.className = "hues-m-nextbutton"; + images.appendChild(this.imageList); + imageControls.appendChild(this.imagePrev); + imageControls.appendChild(this.imageMode); + imageControls.appendChild(this.imageNext); + images.appendChild(imageControls); + rightBox.appendChild(images); + + let leftInfo = document.createElement("div"); + leftInfo.className = "hues-m-leftinfo"; + let rightInfo = document.createElement("div"); + rightInfo.className = "hues-m-rightinfo"; + leftInfo.appendChild(this.xBlur); + leftInfo.appendChild(this.yBlur); + rightInfo.appendChild(this.timer); + rightInfo.appendChild(this.beatCount); + this.rightInfo = rightInfo; + this.leftInfo = leftInfo; + controls.appendChild(leftInfo); + controls.appendChild(rightInfo); + + this.visualiserContainer.className = "hues-m-visualisercontainer"; + controls.appendChild(this.visualiserContainer); + + let beatBar = document.createElement("div"); + beatBar.className = "hues-m-beatbar"; + this.root.appendChild(beatBar); + this.beatBar = beatBar; + + let beatLeft = document.createElement("div"); + beatLeft.className = "hues-m-beatleft"; + beatBar.appendChild(beatLeft); + this.beatLeft = beatLeft; + + let beatRight = document.createElement("div"); + beatRight.className = "hues-m-beatright"; + beatBar.appendChild(beatRight); + this.beatRight = beatRight; - if(["x", "o", "X", "O"].indexOf(beats[0]) != -1) { let beatCenter = document.createElement("div"); - beatCenter.className = "hues-w-beataccent"; - let rot = this.round10(15 - Math.random() * 30); - let x = this.round10(- this.xVariance / 2 + Math.random() * this.xVariance); - let y = this.round10(30 - this.yVariance / 2 + Math.random() * this.yVariance); - let transform = "rotate(" + rot + "deg) translate(" + x + "px, " + y + "px)"; - beatCenter.style.MozTransform = transform; - beatCenter.style.webkitTransform = transform; - beatCenter.style.transform = transform; - beatCenter.textContent = beats[0].toUpperCase(); + beatCenter.className = "hues-m-beatcenter"; this.root.appendChild(beatCenter); - window.setTimeout(this.removeBeat.bind(this, beatCenter), 1500); - } -}; - -WeedUI.prototype.round10 = function(num) { - return Math.round(num * 10) / 10; -}; - -WeedUI.prototype.removeBeat = function(element) { - this.root.removeChild(element); -}; - -function ModernUI(parent, name) { - this.beatBar = null; - this.beatLeft = null; - this.beatRight = null; - this.beatCenter = null; - this.rightBox = null; - this.leftBox = null; - this.rightInfo = null; - this.leftInfo = null; - this.controls = null; - this.volInput = null; - this.volLabel = null; - this.hideRestore = null; - - this.textSize_normal = 0; - this.textSize_small = 0; - this.songLink_size = 0; - this.imageLink_size = 0; + this.beatCenter = beatCenter; + + this.hideRestore = document.createElement("div"); + this.hideRestore.className = "hues-m-hiderestore"; + this.hideRestore.onclick = () => { + this.toggleHide(); + }; + this.root.appendChild(this.hideRestore); + + this.listContainer.className = "hues-m-listcontainer"; + this.root.appendChild(this.listContainer); + + this.addCoreCallback("beat", this.beat.bind(this)); + this.addCoreCallback("newmode", this.newMode.bind(this)); + } + + toggleHide() { + // classList is new-ish, but if you have web audio you'll have this + this.beatBar.classList.remove("hues-ui--hidden"); + this.beatCenter.classList.remove("hues-ui--hidden"); + this.controls.classList.remove("hues-ui--hidden"); + this.hideRestore.classList.remove("hues-ui--hidden"); + switch(this.hidden) { + case 1: + this.beatBar.classList.add("hues-ui--hidden"); + this.beatCenter.classList.add("hues-ui--hidden"); + /* falls through */ + case 0: + this.controls.classList.add("hues-ui--hidden"); + this.hideRestore.classList.add("hues-ui--hidden"); + } + this.hidden = (this.hidden+1) % 3; + } - this.currentBeat = "."; + updateVolume(vol) { + this.volInput.value = vol; + if(vol === 0) { + this.volLabel.textContent = "(VOL)"; + } else { + this.volLabel.textContent = "VOL"; + } + } - HuesUI.call(this, parent, name ? name : "ModernUI"); + newMode (isAuto) { + if(isAuto) { + this.imageMode.innerHTML = ''; // PAUSE; + } else { + this.imageMode.innerHTML = ""; // PLAY + } + } - this.hidden = 0; // we have a 3 stage hide -} + beat(beats, index) { + this.currentBeat = beats[0]; + let rest = beats.slice(1); -ModernUI.prototype = Object.create(HuesUI.prototype); -ModernUI.prototype.constructor = ModernUI; - -ModernUI.prototype.initUI = function() { - HuesUI.prototype.initUI.call(this); - - this.imageName.className = "hues-m-imagename"; - this.songName.className = "hues-m-songtitle"; - - let controls = document.createElement("div"); - controls.className = "hues-m-controls"; - this.root.appendChild(controls); - this.controls = controls; - - controls.appendChild(this.imageName); - controls.appendChild(this.songName); - - let leftBox = document.createElement("div"); - leftBox.className = "hues-m-leftbox"; - controls.appendChild(leftBox); - this.leftBox = leftBox; - - this.hueName.className = "hues-m-huename"; - leftBox.appendChild(this.hueName); - - let volCluster = document.createElement("div"); - volCluster.className = "hues-m-vol-cluster"; - leftBox.appendChild(volCluster); - - this.settingsToggle.className += " hues-m-cog"; - volCluster.appendChild(this.settingsToggle); - - this.hideToggle.className = "hues-m-hide"; - volCluster.appendChild(this.hideToggle); - - let volBar = document.createElement("div"); - volBar.className = "hues-m-vol-bar"; - this.volBar = volBar; - volCluster.appendChild(volBar); - - let label = document.createElement("div"); - label.textContent = "VOL"; - label.className = "hues-m-vol-label"; - label.onclick = () => { - this.core.soundManager.toggleMute(); - }; - volBar.appendChild(label); - this.volLabel = label; - - this.infoToggle = document.createElement("div"); - this.infoToggle.innerHTML = '?'; - this.infoToggle.className = "hues-m-question"; - this.infoToggle.onclick = () => { - this.core.window.selectTab("INFO"); - }; - volCluster.appendChild(this.infoToggle); - - let input = document.createElement("input"); - input.type = "range"; - input.min = 0; - input.max = 1; - input.step = 0.1; - volBar.appendChild(input); - this.volInput = input; - input.oninput = () => { - this.core.soundManager.setVolume(parseFloat(input.value)); - }; - - let rightBox = document.createElement("div"); - rightBox.className = "hues-m-rightbox"; - controls.appendChild(rightBox); - this.rightBox = rightBox; - - //Song/image controls - let songs = document.createElement("div"); - songs.className = "hues-m-controlblock"; - this.songBlock = songs; - this.songList.className = "hues-m-songbutton"; - - let songControls = document.createElement("div"); - songControls.className = "hues-m-controlbuttons"; - this.songPrev.className = "hues-m-prevbutton"; - this.songNext.className = "hues-m-nextbutton"; - this.songShuffle = document.createElement("div"); - this.songShuffle.innerHTML = ''; // SHUFFLE - this.songShuffle.className = "hues-m-actbutton hues-icon"; - this.songShuffle.onclick = () => {this.core.randomSong();}; - songs.appendChild(this.songList); - songControls.appendChild(this.songPrev); - songControls.appendChild(this.songShuffle); - songControls.appendChild(this.songNext); - songs.appendChild(songControls); - rightBox.appendChild(songs); - - let images = document.createElement("div"); - images.className = "hues-m-controlblock"; - this.imageList.className = "hues-m-songbutton"; - this.imageBlock = images; - - let imageControls = document.createElement("div"); - imageControls.className = "hues-m-controlbuttons"; - - this.imageMode = document.createElement("div"); - this.imageMode.innerHTML = ""; // PLAY - this.imageMode.className = "hues-m-actbutton hues-icon"; - this.imageMode.onclick = () => {this.core.toggleFullAuto();}; - this.imagePrev.className = "hues-m-prevbutton"; - this.imageNext.className = "hues-m-nextbutton"; - images.appendChild(this.imageList); - imageControls.appendChild(this.imagePrev); - imageControls.appendChild(this.imageMode); - imageControls.appendChild(this.imageNext); - images.appendChild(imageControls); - rightBox.appendChild(images); - - let leftInfo = document.createElement("div"); - leftInfo.className = "hues-m-leftinfo"; - let rightInfo = document.createElement("div"); - rightInfo.className = "hues-m-rightinfo"; - leftInfo.appendChild(this.xBlur); - leftInfo.appendChild(this.yBlur); - rightInfo.appendChild(this.timer); - rightInfo.appendChild(this.beatCount); - this.rightInfo = rightInfo; - this.leftInfo = leftInfo; - controls.appendChild(leftInfo); - controls.appendChild(rightInfo); - - this.visualiserContainer.className = "hues-m-visualisercontainer"; - controls.appendChild(this.visualiserContainer); - - let beatBar = document.createElement("div"); - beatBar.className = "hues-m-beatbar"; - this.root.appendChild(beatBar); - this.beatBar = beatBar; - - let beatLeft = document.createElement("div"); - beatLeft.className = "hues-m-beatleft"; - beatBar.appendChild(beatLeft); - this.beatLeft = beatLeft; - - let beatRight = document.createElement("div"); - beatRight.className = "hues-m-beatright"; - beatBar.appendChild(beatRight); - this.beatRight = beatRight; - - let beatCenter = document.createElement("div"); - beatCenter.className = "hues-m-beatcenter"; - this.root.appendChild(beatCenter); - this.beatCenter = beatCenter; - - this.hideRestore = document.createElement("div"); - this.hideRestore.className = "hues-m-hiderestore"; - this.hideRestore.onclick = () => { - this.toggleHide(); - }; - this.root.appendChild(this.hideRestore); - - this.listContainer.className = "hues-m-listcontainer"; - this.root.appendChild(this.listContainer); - - this.addCoreCallback("beat", this.beat.bind(this)); - this.addCoreCallback("newmode", this.newMode.bind(this)); -}; - -ModernUI.prototype.toggleHide = function() { - // classList is new-ish, but if you have web audio you'll have this - this.beatBar.classList.remove("hues-ui--hidden"); - this.beatCenter.classList.remove("hues-ui--hidden"); - this.controls.classList.remove("hues-ui--hidden"); - this.hideRestore.classList.remove("hues-ui--hidden"); - switch(this.hidden) { - case 1: - this.beatBar.classList.add("hues-ui--hidden"); - this.beatCenter.classList.add("hues-ui--hidden"); - /* falls through */ - case 0: - this.controls.classList.add("hues-ui--hidden"); - this.hideRestore.classList.add("hues-ui--hidden"); + this.beatLeft.textContent = rest; + this.beatRight.textContent = rest; + + + if (this.currentBeat != ".") { + while (this.beatCenter.firstElementChild) { + this.beatCenter.removeChild(this.beatCenter.firstElementChild); + } + let span = this.beatCenter.ownerDocument.createElement("span"); + span.textContent = this.currentBeat; + this.beatCenter.appendChild(span); + } + this.beatCount.textContent = "B=" + this.intToHex(index, 4); } - this.hidden = (this.hidden+1) % 3; -}; -ModernUI.prototype.updateVolume = function(vol) { - this.volInput.value = vol; - if(vol === 0) { - this.volLabel.textContent = "(VOL)"; - } else { - this.volLabel.textContent = "VOL"; + // get the width of a single character in the link box for a given classname + textWidth(className) { + // Could be song or image link, don't care + let el = this.songLink; + let oldContent = el.innerHTML; + + // offsetWidth is rounded, divide by 100 + let text = ""; + for(let i = 0; i < 100; i++) { + text += " "; + } + el.innerHTML = text; + // We override this just after so don't bother to restore it + el.className = className; + let size = el.offsetWidth / 100; + + el.innerHTML = oldContent; + + return size; } -}; -ModernUI.prototype.newMode = function(isAuto) { - if(isAuto) { - this.imageMode.innerHTML = ''; // PAUSE; - } else { - this.imageMode.innerHTML = ""; // PLAY + resize() { + this.textSize_normal = this.textWidth(""); + this.textSize_small = this.textWidth("small"); + this.songLink_size = this.songName.clientWidth; + this.imageLink_size = this.imageName.clientWidth; + + this.resizeSong(); + this.resizeImage(); + this.core.visualiser.width = this.controls.offsetWidth; + this.core.resizeVisualiser(); } -}; -ModernUI.prototype.beat = function(beats, index) { - this.currentBeat = beats[0]; - let rest = beats.slice(1); + resizeElement(el, parentSize) { + let chars = el.textContent.length; + if (chars * this.textSize_normal < parentSize) { + el.className = ""; + } else if(chars * this.textSize_small < parentSize) { + el.className = "small"; + } else { + el.className = "x-small"; + } + } - this.beatLeft.textContent = rest; - this.beatRight.textContent = rest; + resizeSong() { + this.resizeElement(this.songLink, this.songLink_size); + } + + resizeImage() { + this.resizeElement(this.imageLink, this.imageLink_size); + } + newSong(song) { + super.newSong(song); - if (this.currentBeat != ".") { - while (this.beatCenter.firstElementChild) { - this.beatCenter.removeChild(this.beatCenter.firstElementChild); + if(!song) { + return; } - let span = this.beatCenter.ownerDocument.createElement("span"); - span.textContent = this.currentBeat; - this.beatCenter.appendChild(span); - } - this.beatCount.textContent = "B=" + this.intToHex(index, 4); -}; - -// get the width of a single character in the link box for a given classname -ModernUI.prototype.textWidth = function(className) { - // Could be song or image link, don't care - let el = this.songLink; - let oldContent = el.innerHTML; - - // offsetWidth is rounded, divide by 100 - let text = ""; - for(let i = 0; i < 100; i++) { - text += " "; - } - el.innerHTML = text; - // We override this just after so don't bother to restore it - el.className = className; - let size = el.offsetWidth / 100; - - el.innerHTML = oldContent; - - return size; + + this.resizeSong(); + } + + newImage(image) { + super.newImage(image); + + if(!image) { + return; + } + + this.resizeImage(); + } } -ModernUI.prototype.resize = function() { - this.textSize_normal = this.textWidth(""); - this.textSize_small = this.textWidth("small"); - this.songLink_size = this.songName.clientWidth; - this.imageLink_size = this.imageName.clientWidth; +class XmasUI extends ModernUI { + constructor(parent, name) { + super(parent, name ? name : "XmasUI"); + this.initSnow(); + + // This will cache our inverted lights images + this.invert(true); + + this.controls.removeChild(this.leftBox); + this.controls.removeChild(this.rightBox); + this.controls.removeChild(this.rightInfo); + this.controls.removeChild(this.leftInfo); + + this.leftBox = null; + this.rightBox = null; + this.hueName = null; + this.xBlur = null; + this.yBlur = null; + this.timer = null; + + this.controls.className += " hues-x-controls"; + this.beatBar.className += " hues-x-beatbar"; + + this.lights = []; + + let wires = document.createElement("div"); + wires.className = "hues-x-wires"; + + let left = document.createElement("div"); + left.className = "hues-x-wiresleft"; + xleft.forEach(function(l, i, a) { + let light = this.newLight(l, left); + light.style.transform = "rotate(" + l.angle + "deg)"; + light.style.left = l.x + "px"; + light.style.top = l.y + "px"; + this.lights.push(light); + }, this); + + let right = document.createElement("div"); + right.className = "hues-x-wiresright"; + xright.forEach(function(l, i, a) { + let light = this.newLight(l, right); + light.style.transform = "rotate(" + (-l.angle) + "deg)"; + light.style.right = l.x + "px"; + light.style.top = l.y + "px"; + this.lights.push(light); + }, this); - this.resizeSong(); - this.resizeImage(); - this.core.visualiser.width = this.controls.offsetWidth; - this.core.resizeVisualiser(); -}; + let bottomHelper = document.createElement("div"); + bottomHelper.className = "hues-x-wiresbottomhelper"; + let bottom = document.createElement("div"); + bottom.className = "hues-x-wiresbottom"; + xbottom.forEach(function(l, i, a) { + let light = this.newLight(l, bottom); + light.style.transform = "rotate(" + l.angle + "deg)"; + light.style.left = l.x + "px"; + light.style.bottom = l.y + "px"; + this.lights.push(light); + }, this); -ModernUI.prototype.resizeElement = function(el, parentSize) { - let chars = el.textContent.length; - if (chars * this.textSize_normal < parentSize) { - el.className = ""; - } else if(chars * this.textSize_small < parentSize) { - el.className = "small"; - } else { - el.className = "x-small"; + wires.appendChild(left); + wires.appendChild(right); + bottomHelper.appendChild(bottom); + wires.appendChild(bottomHelper); + this.root.appendChild(wires); + + this.visualiserContainer.className = "hues-x-visualisercontainer"; + this.controls.removeChild(this.visualiserContainer); + this.beatBar.appendChild(this.visualiserContainer); + } + + invert(invert) { + super.invert(invert); + + if(invert) { + this.snowContext.fillStyle = "rgba(0, 0, 0, 0.8)"; + } else { + this.snowContext.fillStyle = "rgba(255, 255, 255, 0.8)"; + } } -}; -ModernUI.prototype.resizeSong = function() { - this.resizeElement(this.songLink, this.songLink_size); -}; + connectCore(core) { + super.connectCore(core); + this.startSnow(); + } -ModernUI.prototype.resizeImage = function() { - this.resizeElement(this.imageLink, this.imageLink_size); -}; + disconnect() { + this.stopSnow(); + super.disconnect(); + } -ModernUI.prototype.newSong = function(song) { - HuesUI.prototype.newSong.call(this, song); + lightOn(light) { + light.on.className = "hues-x-lighton"; + light.off.className = "hues-x-lightoff"; + } - if(!song) { - return; + lightOff(light) { + light.on.className = "hues-x-lighton off"; + light.off.className = "hues-x-lightoff off"; } - this.resizeSong(); -}; + lightFadeOut(light) { + light.on.className = "hues-x-lighton hues-x-fade off"; + light.off.className = "hues-x-lightoff hues-x-fade off"; + } -ModernUI.prototype.newImage = function(image) { - HuesUI.prototype.newImage.call(this, image); + lightRecolour(light) { + let hue = Math.floor(Math.random() * 7) * -56; + light.on.style.backgroundPosition = hue + "px 0"; + light.off.style.backgroundPosition = hue + "px 0"; + } - if(!image) { - return; + randomLight(light) { + if(Math.random() >= 0.5) { + this.lightOn(light); + } else { + this.lightOff(light); + } } - this.resizeImage(); -}; + newLight(l, parent) { + let light = document.createElement("div"); + light.className = "hues-x-light"; + let bulb = document.createElement("div"); + let on = document.createElement("div"); + let off = document.createElement("div"); + bulb.appendChild(on); + bulb.appendChild(off); + light.appendChild(bulb); + parent.appendChild(light); + light.on = on; + light.off = off; + light.bulb = bulb; + this.randomLight(light); + this.lightRecolour(light); + return light; + } -function XmasUI(parent, name) { - ModernUI.call(this, parent, name ? name : "XmasUI"); - this.initSnow(); - - // This will cache our inverted lights images - this.invert(true); - - this.controls.removeChild(this.leftBox); - this.controls.removeChild(this.rightBox); - this.controls.removeChild(this.rightInfo); - this.controls.removeChild(this.leftInfo); - - this.leftBox = null; - this.rightBox = null; - this.hueName = null; - this.xBlur = null; - this.yBlur = null; - this.timer = null; - - this.controls.className += " hues-x-controls"; - this.beatBar.className += " hues-x-beatbar"; - - this.lights = []; - - let wires = document.createElement("div"); - wires.className = "hues-x-wires"; - - let left = document.createElement("div"); - left.className = "hues-x-wiresleft"; - xleft.forEach(function(l, i, a) { - let light = this.newLight(l, left); - light.style.transform = "rotate(" + l.angle + "deg)"; - light.style.left = l.x + "px"; - light.style.top = l.y + "px"; - this.lights.push(light); - }, this); - - let right = document.createElement("div"); - right.className = "hues-x-wiresright"; - xright.forEach(function(l, i, a) { - let light = this.newLight(l, right); - light.style.transform = "rotate(" + (-l.angle) + "deg)"; - light.style.right = l.x + "px"; - light.style.top = l.y + "px"; - this.lights.push(light); - }, this); - - let bottomHelper = document.createElement("div"); - bottomHelper.className = "hues-x-wiresbottomhelper"; - let bottom = document.createElement("div"); - bottom.className = "hues-x-wiresbottom"; - xbottom.forEach(function(l, i, a) { - let light = this.newLight(l, bottom); - light.style.transform = "rotate(" + l.angle + "deg)"; - light.style.left = l.x + "px"; - light.style.bottom = l.y + "px"; - this.lights.push(light); - }, this); - - wires.appendChild(left); - wires.appendChild(right); - bottomHelper.appendChild(bottom); - wires.appendChild(bottomHelper); - this.root.appendChild(wires); - - this.visualiserContainer.className = "hues-x-visualisercontainer"; - this.controls.removeChild(this.visualiserContainer); - this.beatBar.appendChild(this.visualiserContainer); -} + beat(beats, index) { + super.beat(beats, index); + if(this.currentBeat != ".") { + this.lights.forEach(function(light, i, a) { + switch(this.currentBeat) { + case ":": + this.lightOn(light); + this.lightRecolour(light); + break; + case "+": + this.lightFadeOut(light); + break; + default: + this.randomLight(light); + } + }, this); + } + } -XmasUI.prototype = Object.create(ModernUI.prototype); -XmasUI.prototype.constructor = XmasUI; + initSnow() { + this.snowCanvas = document.createElement("canvas"); + this.snowContext = this.snowCanvas.getContext("2d"); + this.snowCanvas.width = 1280; + this.snowCanvas.height = 720; + this.snowCanvas.style.display = "none"; + this.snowCanvas.className = "hues-canvas hues-x-snow"; + + this.root.appendChild(this.snowCanvas); + + this.snowing = false; + this.maxSnow = 30; + this.snowAngle = 0; + this.lastSnow = 0; + this.snowflakes = []; + + this.addCoreCallback("frame", this.drawSnow.bind(this)); + } -XmasUI.prototype.invert = function(invert) { - HuesUI.prototype.invert.call(this, invert); - - if(invert) { - this.snowContext.fillStyle = "rgba(0, 0, 0, 0.8)"; - } else { - this.snowContext.fillStyle = "rgba(255, 255, 255, 0.8)"; - } -}; - -XmasUI.prototype.connectCore = function(core) { - HuesUI.prototype.connectCore.call(this, core); - this.startSnow(); -}; - -XmasUI.prototype.disconnect = function() { - this.stopSnow(); - HuesUI.prototype.disconnect.call(this); -}; - -XmasUI.prototype.lightOn = function(light) { - light.on.className = "hues-x-lighton"; - light.off.className = "hues-x-lightoff"; -}; - -XmasUI.prototype.lightOff = function(light) { - light.on.className = "hues-x-lighton off"; - light.off.className = "hues-x-lightoff off"; -}; - -XmasUI.prototype.lightFadeOut = function(light) { - light.on.className = "hues-x-lighton hues-x-fade off"; - light.off.className = "hues-x-lightoff hues-x-fade off"; -}; - -XmasUI.prototype.lightRecolour = function(light) { - let hue = Math.floor(Math.random() * 7) * -56; - light.on.style.backgroundPosition = hue + "px 0"; - light.off.style.backgroundPosition = hue + "px 0"; -}; - -XmasUI.prototype.randomLight = function(light) { - if(Math.random() >= 0.5) { - this.lightOn(light); - } else { - this.lightOff(light); - } -}; - -XmasUI.prototype.newLight = function(l, parent) { - let light = document.createElement("div"); - light.className = "hues-x-light"; - let bulb = document.createElement("div"); - let on = document.createElement("div"); - let off = document.createElement("div"); - bulb.appendChild(on); - bulb.appendChild(off); - light.appendChild(bulb); - parent.appendChild(light); - light.on = on; - light.off = off; - light.bulb = bulb; - this.randomLight(light); - this.lightRecolour(light); - return light; -}; - -XmasUI.prototype.beat = function(beats, index) { - ModernUI.prototype.beat.call(this, beats, index); - if(this.currentBeat != ".") { - this.lights.forEach(function(light, i, a) { - switch(this.currentBeat) { - case ":": - this.lightOn(light); - this.lightRecolour(light); - break; - case "+": - this.lightFadeOut(light); - break; - default: - this.randomLight(light); - } - }, this); + // From http://thecodeplayer.com/walkthrough/html5-canvas-snow-effect + + startSnow() { + this.snowing = true; + this.snowCanvas.style.display = "block"; + let height = this.snowCanvas.height; + let width = this.snowCanvas.width; + this.snowAngle = 0; + this.snowflakes = []; + for(let i = 0; i < this.maxSnow; i++) { + this.snowflakes.push({ + x: Math.random()*width, //x-coordinate + y: Math.random()*height, //y-coordinate + r: Math.random()*4+1, //radius + d: Math.random()*25 //density + }); + } + this.lastSnow = Date.now() / 1000; } -}; -XmasUI.prototype.initSnow = function() { - this.snowCanvas = document.createElement("canvas"); - this.snowContext = this.snowCanvas.getContext("2d"); - this.snowCanvas.width = 1280; - this.snowCanvas.height = 720; - this.snowCanvas.style.display = "none"; - this.snowCanvas.className = "hues-canvas hues-x-snow"; - - this.root.appendChild(this.snowCanvas); + stopSnow() { + this.snowing = false; + this.snowCanvas.style.display = "none"; + } - this.snowing = false; - this.maxSnow = 30; - this.snowAngle = 0; - this.lastSnow = 0; - this.snowflakes = []; - - this.addCoreCallback("frame", this.drawSnow.bind(this)); -}; - -// From http://thecodeplayer.com/walkthrough/html5-canvas-snow-effect - -XmasUI.prototype.startSnow = function() { - this.snowing = true; - this.snowCanvas.style.display = "block"; - let height = this.snowCanvas.height; - let width = this.snowCanvas.width; - this.snowAngle = 0; - this.snowflakes = []; - for(let i = 0; i < this.maxSnow; i++) { - this.snowflakes.push({ - x: Math.random()*width, //x-coordinate - y: Math.random()*height, //y-coordinate - r: Math.random()*4+1, //radius - d: Math.random()*25 //density - }); - } - this.lastSnow = Date.now() / 1000; -}; - -XmasUI.prototype.stopSnow = function() { - this.snowing = false; - this.snowCanvas.style.display = "none"; -}; - -XmasUI.prototype.drawSnow = function() { - let width = this.snowCanvas.width; - let height = this.snowCanvas.height; - let now = Date.now() / 1000; - let delta = this.lastSnow - now; - this.lastSnow = now; - this.snowContext.clearRect(0, 0, width, height); - - this.snowContext.beginPath(); - for(let i = 0; i < this.maxSnow; i++) { - let flake = this.snowflakes[i]; - this.snowContext.moveTo(flake.x, flake.y); - this.snowContext.arc(flake.x, flake.y, flake.r, 0, Math.PI * 2, true); - } - this.snowContext.fill(); - - this.snowAngle += delta / 6; - for(let i = 0; i < this.maxSnow; i++) { - let flake = this.snowflakes[i]; - //Updating X and Y coordinates - //We will add 1 to the cos function to prevent negative values which will lead flakes to move upwards - //Every particle has its own density which can be used to make the downward movement different for each flake - //Lets make it more random by adding in the radius - flake.y += Math.cos(this.snowAngle + flake.d) + 1 + flake.r / 2; - flake.x += Math.sin(this.snowAngle) * 2; - - //Sending flakes back from the top when it exits - //Lets make it a bit more organic and let flakes enter from the left and right also. - if(flake.x > width + 5 || flake.x < -5 || flake.y > height) { - if(i % 3 > 0) {//66.67% of the flakes - this.snowflakes[i] = {x: Math.random() * width, y: -10, r: flake.r, d: flake.d}; - } - else { - //If the flake is exitting from the right - if(Math.sin(this.snowAngle) > 0) { - //Enter from the left - this.snowflakes[i] = {x: -5, y: Math.random() * height, r: flake.r, d: flake.d}; + drawSnow() { + let width = this.snowCanvas.width; + let height = this.snowCanvas.height; + let now = Date.now() / 1000; + let delta = this.lastSnow - now; + this.lastSnow = now; + this.snowContext.clearRect(0, 0, width, height); + + this.snowContext.beginPath(); + for(let i = 0; i < this.maxSnow; i++) { + let flake = this.snowflakes[i]; + this.snowContext.moveTo(flake.x, flake.y); + this.snowContext.arc(flake.x, flake.y, flake.r, 0, Math.PI * 2, true); + } + this.snowContext.fill(); + + this.snowAngle += delta / 6; + for(let i = 0; i < this.maxSnow; i++) { + let flake = this.snowflakes[i]; + //Updating X and Y coordinates + //We will add 1 to the cos function to prevent negative values which will lead flakes to move upwards + //Every particle has its own density which can be used to make the downward movement different for each flake + //Lets make it more random by adding in the radius + flake.y += Math.cos(this.snowAngle + flake.d) + 1 + flake.r / 2; + flake.x += Math.sin(this.snowAngle) * 2; + + //Sending flakes back from the top when it exits + //Lets make it a bit more organic and let flakes enter from the left and right also. + if(flake.x > width + 5 || flake.x < -5 || flake.y > height) { + if(i % 3 > 0) {//66.67% of the flakes + this.snowflakes[i] = {x: Math.random() * width, y: -10, r: flake.r, d: flake.d}; } else { - //Enter from the right - this.snowflakes[i] = {x: width+5, y: Math.random() * height, r: flake.r, d: flake.d}; + //If the flake is exitting from the right + if(Math.sin(this.snowAngle) > 0) { + //Enter from the left + this.snowflakes[i] = {x: -5, y: Math.random() * height, r: flake.r, d: flake.d}; + } + else { + //Enter from the right + this.snowflakes[i] = {x: width+5, y: Math.random() * height, r: flake.r, d: flake.d}; + } } } } } -}; -XmasUI.prototype.resize = function() { - ModernUI.prototype.resize.call(this); - - let ratio = window.innerWidth / window.innerHeight; - // cleared on resize - let savedFill = this.snowContext.fillStyle; - this.snowCanvas.width = Math.ceil(720 * ratio); - this.snowContext.fillStyle = savedFill; -}; - -XmasUI.prototype.newColour = function(colour) {}; -XmasUI.prototype.blurUpdated = function(x, y) {}; -XmasUI.prototype.updateTime = function(time) {}; - -function HalloweenUI(parent, name) { - ModernUI.call(this, parent, name ? name : "HalloweenUI"); - // This will cache our inverted tombstone image - this.invert(true); + resize() { + super.resize(); + + let ratio = window.innerWidth / window.innerHeight; + // cleared on resize + let savedFill = this.snowContext.fillStyle; + this.snowCanvas.width = Math.ceil(720 * ratio); + this.snowContext.fillStyle = savedFill; + } + + newColour(colour) {} + blurUpdated(x, y) {} + updateTime(time) {} } -HalloweenUI.prototype = Object.create(ModernUI.prototype); -HalloweenUI.prototype.constructor = HalloweenUI; +class HalloweenUI extends ModernUI { + constructor(parent, name) { + super(parent, name ? name : "HalloweenUI"); + // This will cache our inverted tombstone image + this.invert(true); + } -HalloweenUI.prototype.initUI = function() { - ModernUI.prototype.initUI.call(this); - - this.controls.className += " hues-h-controls"; - this.beatBar.className += " hues-h-beatbar"; - this.leftBox.className += " hues-h-leftbox"; - this.rightBox.className += " hues-h-rightbox"; - this.volBar.className += " hues-h-vol-bar"; - - this.beatLeft.className += " hues-h-text"; - this.beatRight.className += " hues-h-text"; - this.beatCenter.className += " hues-h-text"; - this.songShuffle.className += " hues-h-text"; - this.songNext.className += " hues-h-text"; - this.songPrev.className += " hues-h-text"; - this.songList.className += " hues-h-text"; - this.songName.className += " hues-h-text"; - this.imageMode.className += " hues-h-text"; - this.imageNext.className += " hues-h-text"; - this.imagePrev.className += " hues-h-text"; - this.imageList.className += " hues-h-text"; - this.imageName.className += " hues-h-text"; - this.hueName.className += " hues-h-text"; - - this.settingsToggle.className += " hues-h-text"; - this.hideToggle.className += " hues-h-text"; - this.infoToggle.className += " hues-h-text"; - this.volLabel.className += " hues-h-text"; - - this.timer.className = "hues-h-textfade"; - this.beatCount.className = "hues-h-textfade"; - this.xBlur.className = "hues-h-textfade"; - this.yBlur.className = "hues-h-textfade"; - - let leftBoxTomb = document.createElement("div"); - leftBoxTomb.className = "hues-h-tombstone"; - this.leftBox.appendChild(leftBoxTomb); - - let songTomb = document.createElement("div"); - songTomb.className = "hues-h-tombstone"; - this.songBlock.insertBefore(songTomb,this.songBlock.firstChild); - - let imageTomb = document.createElement("div"); - imageTomb.className = "hues-h-tombstone"; - this.imageBlock.insertBefore(imageTomb,this.imageBlock.firstChild); - - let topLeft = document.createElement("div"); - topLeft.className = "hues-h-topleft"; - let topRight = document.createElement("div"); - topRight.className = "hues-h-topright"; - let bottomRight = document.createElement("div"); - bottomRight.className = "hues-h-bottomright"; - - this.root.appendChild(topLeft); - this.root.appendChild(topRight); - this.root.appendChild(bottomRight); - - let leftHand = document.createElement("div"); - leftHand.className = "hues-h-left-hand"; - this.beatBar.appendChild(leftHand); - let rightHand = document.createElement("div"); - rightHand.className = "hues-h-right-hand"; - this.beatBar.appendChild(rightHand); - - this.vignette = document.createElement("div"); - this.vignette.className = "hues-h-vignette"; - this.root.appendChild(this.vignette); -}; + initUI() { + super.initUI(); + + this.controls.className += " hues-h-controls"; + this.beatBar.className += " hues-h-beatbar"; + this.leftBox.className += " hues-h-leftbox"; + this.rightBox.className += " hues-h-rightbox"; + this.volBar.className += " hues-h-vol-bar"; + + this.beatLeft.className += " hues-h-text"; + this.beatRight.className += " hues-h-text"; + this.beatCenter.className += " hues-h-text"; + this.songShuffle.className += " hues-h-text"; + this.songNext.className += " hues-h-text"; + this.songPrev.className += " hues-h-text"; + this.songList.className += " hues-h-text"; + this.songName.className += " hues-h-text"; + this.imageMode.className += " hues-h-text"; + this.imageNext.className += " hues-h-text"; + this.imagePrev.className += " hues-h-text"; + this.imageList.className += " hues-h-text"; + this.imageName.className += " hues-h-text"; + this.hueName.className += " hues-h-text"; + + this.settingsToggle.className += " hues-h-text"; + this.hideToggle.className += " hues-h-text"; + this.infoToggle.className += " hues-h-text"; + this.volLabel.className += " hues-h-text"; + + this.timer.className = "hues-h-textfade"; + this.beatCount.className = "hues-h-textfade"; + this.xBlur.className = "hues-h-textfade"; + this.yBlur.className = "hues-h-textfade"; + + let leftBoxTomb = document.createElement("div"); + leftBoxTomb.className = "hues-h-tombstone"; + this.leftBox.appendChild(leftBoxTomb); + + let songTomb = document.createElement("div"); + songTomb.className = "hues-h-tombstone"; + this.songBlock.insertBefore(songTomb,this.songBlock.firstChild); + + let imageTomb = document.createElement("div"); + imageTomb.className = "hues-h-tombstone"; + this.imageBlock.insertBefore(imageTomb,this.imageBlock.firstChild); + + let topLeft = document.createElement("div"); + topLeft.className = "hues-h-topleft"; + let topRight = document.createElement("div"); + topRight.className = "hues-h-topright"; + let bottomRight = document.createElement("div"); + bottomRight.className = "hues-h-bottomright"; + + this.root.appendChild(topLeft); + this.root.appendChild(topRight); + this.root.appendChild(bottomRight); + + let leftHand = document.createElement("div"); + leftHand.className = "hues-h-left-hand"; + this.beatBar.appendChild(leftHand); + let rightHand = document.createElement("div"); + rightHand.className = "hues-h-right-hand"; + this.beatBar.appendChild(rightHand); + + this.vignette = document.createElement("div"); + this.vignette.className = "hues-h-vignette"; + this.root.appendChild(this.vignette); + } -HalloweenUI.prototype.beat = function(beats, index) { - ModernUI.prototype.beat.call(this, beats, index); - - if (this.currentBeat != ".") { - let eyes = this.beatCenter.ownerDocument.createElement("div"); - eyes.className = "hues-m-beatcenter hues-h-eyes"; - this.beatCenter.appendChild(eyes); + beat(beats, index) { + super.beat(beats, index); + + if (this.currentBeat != ".") { + let eyes = this.beatCenter.ownerDocument.createElement("div"); + eyes.className = "hues-m-beatcenter hues-h-eyes"; + this.beatCenter.appendChild(eyes); + } } -}; -HalloweenUI.prototype.connectCore = function(core) { - ModernUI.prototype.connectCore.call(this, core); - - this.core.preloader.classList.add("hues-h-text"); -}; + connectCore(core) { + super.connectCore(core); + + this.core.preloader.classList.add("hues-h-text"); + } -HalloweenUI.prototype.disconnect = function() { - this.core.preloader.classList.remove("hues-h-text"); - - ModernUI.prototype.disconnect.call(this); -}; + disconnect() { + this.core.preloader.classList.remove("hues-h-text"); + + super.disconnect(); + } +} // Positions and angles for the Xmas lights let xleft = [ diff --git a/src/js/HuesWindow.js b/src/js/HuesWindow.js index ad035b4..7805db9 100644 --- a/src/js/HuesWindow.js +++ b/src/js/HuesWindow.js @@ -22,149 +22,151 @@ (function(window, document) { "use strict"; -function HuesWindow(root, defaults) { - this.eventListeners = { - /* callback windowshown(shown) - * - * When the window is shown, hidden or toggled this fires. - * 'shown' is true if the window was made visible, false otherwise - */ - windowshown : [], - /* callback tabselected(tabName) - * - * The name of the tab that was selected - */ - tabselected : [] - }; - - this.hasUI = defaults.enableWindow; - - if(!this.hasUI) - return; - - this.window = document.createElement("div"); - this.window.className = "hues-win-helper"; - root.appendChild(this.window); - - let actualWindow = document.createElement("div"); - actualWindow.className = "hues-win"; - this.window.appendChild(actualWindow); - - let closeButton = document.createElement("div"); - closeButton.className = "hues-win__closebtn"; - closeButton.onclick = this.hide.bind(this); - actualWindow.appendChild(closeButton); - - this.tabContainer = document.createElement("div"); - this.tabContainer.className = "hues-win__tabs"; - actualWindow.appendChild(this.tabContainer); - - this.contentContainer = document.createElement("div"); - this.contentContainer.className = "hues-win__content"; - actualWindow.appendChild(this.contentContainer); - - this.contents = []; - this.tabs = []; - this.tabNames = []; +class HuesWindow { + constructor(root, defaults) { + this.eventListeners = { + /* callback windowshown(shown) + * + * When the window is shown, hidden or toggled this fires. + * 'shown' is true if the window was made visible, false otherwise + */ + windowshown : [], + /* callback tabselected(tabName) + * + * The name of the tab that was selected + */ + tabselected : [] + }; + + this.hasUI = defaults.enableWindow; + + if(!this.hasUI) + return; + + this.window = document.createElement("div"); + this.window.className = "hues-win-helper"; + root.appendChild(this.window); + + let actualWindow = document.createElement("div"); + actualWindow.className = "hues-win"; + this.window.appendChild(actualWindow); + + let closeButton = document.createElement("div"); + closeButton.className = "hues-win__closebtn"; + closeButton.onclick = this.hide.bind(this); + actualWindow.appendChild(closeButton); + + this.tabContainer = document.createElement("div"); + this.tabContainer.className = "hues-win__tabs"; + actualWindow.appendChild(this.tabContainer); + + this.contentContainer = document.createElement("div"); + this.contentContainer.className = "hues-win__content"; + actualWindow.appendChild(this.contentContainer); + + this.contents = []; + this.tabs = []; + this.tabNames = []; - - if(defaults.showWindow) { - this.show(); - } else { - this.hide(); + + if(defaults.showWindow) { + this.show(); + } else { + this.hide(); + } } -} -HuesWindow.prototype.addTab = function(tabName, tabContent) { - if(!this.hasUI) - return; - - let label = document.createElement("div"); - label.textContent = tabName; - label.className = "tab-label"; - label.onclick = this.selectTab.bind(this, tabName); - this.tabContainer.appendChild(label); - this.tabs.push(label); - this.tabNames.push(tabName); - - let content = document.createElement("div"); - content.className = "tab-content"; - content.appendChild(tabContent); - this.contentContainer.appendChild(content); - this.contents.push(content); -}; - -HuesWindow.prototype.selectTab = function(tabName, dontShowWin) { - if(!this.hasUI) - return; - if(!dontShowWin) { - this.show(); + addTab(tabName, tabContent) { + if(!this.hasUI) + return; + + let label = document.createElement("div"); + label.textContent = tabName; + label.className = "tab-label"; + label.onclick = this.selectTab.bind(this, tabName); + this.tabContainer.appendChild(label); + this.tabs.push(label); + this.tabNames.push(tabName); + + let content = document.createElement("div"); + content.className = "tab-content"; + content.appendChild(tabContent); + this.contentContainer.appendChild(content); + this.contents.push(content); } - for(let i = 0; i < this.tabNames.length; i++) { - let name = this.tabNames[i]; - if(tabName.toLowerCase() == name.toLowerCase()) { - this.contents[i].classList.add("tab-content--active"); - this.tabs[i].classList.add("tab-label--active"); - this.callEventListeners("tabselected", name); - } else { - this.contents[i].classList.remove("tab-content--active"); - this.tabs[i].classList.remove("tab-label--active"); + + selectTab(tabName, dontShowWin) { + if(!this.hasUI) + return; + if(!dontShowWin) { + this.show(); + } + for(let i = 0; i < this.tabNames.length; i++) { + let name = this.tabNames[i]; + if(tabName.toLowerCase() == name.toLowerCase()) { + this.contents[i].classList.add("tab-content--active"); + this.tabs[i].classList.add("tab-label--active"); + this.callEventListeners("tabselected", name); + } else { + this.contents[i].classList.remove("tab-content--active"); + this.tabs[i].classList.remove("tab-label--active"); + } } } -}; -HuesWindow.prototype.hide = function() { - if(!this.hasUI) - return; - - this.window.classList.add("hidden"); - this.callEventListeners("windowshown", false); -}; + hide() { + if(!this.hasUI) + return; + + this.window.classList.add("hidden"); + this.callEventListeners("windowshown", false); + } -HuesWindow.prototype.show = function() { - if(!this.hasUI) - return; - - this.window.classList.remove("hidden"); - this.callEventListeners("windowshown", true); -}; + show() { + if(!this.hasUI) + return; + + this.window.classList.remove("hidden"); + this.callEventListeners("windowshown", true); + } -HuesWindow.prototype.toggle = function() { - if(!this.hasUI) - return; - if(this.window.classList.contains("hidden")) { - this.show(); - } else { - this.hide(); + toggle() { + if(!this.hasUI) + return; + if(this.window.classList.contains("hidden")) { + this.show(); + } else { + this.hide(); + } } -}; -HuesWindow.prototype.callEventListeners = function(ev) { - let args = Array.prototype.slice.call(arguments, 1); - this.eventListeners[ev].forEach(function(callback) { - callback.apply(null, args); - }); -}; + callEventListeners(ev) { + let args = Array.prototype.slice.call(arguments, 1); + this.eventListeners[ev].forEach(function(callback) { + callback.apply(null, args); + }); + } -HuesWindow.prototype.addEventListener = function(ev, callback) { - ev = ev.toLowerCase(); - if (typeof(this.eventListeners[ev]) !== "undefined") { - this.eventListeners[ev].push(callback); - } else { - throw Error("Unknown event: " + ev); + addEventListener(ev, callback) { + ev = ev.toLowerCase(); + if (typeof(this.eventListeners[ev]) !== "undefined") { + this.eventListeners[ev].push(callback); + } else { + throw Error("Unknown event: " + ev); + } } -}; -HuesWindow.prototype.removeEventListener = function(ev, callback) { - ev = ev.toLowerCase(); - if (typeof(this.eventListeners[ev]) !== "undefined") { - this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { - return (a !== callback); - }); - } else { - throw Error("Unknown event: " + ev); + removeEventListener(ev, callback) { + ev = ev.toLowerCase(); + if (typeof(this.eventListeners[ev]) !== "undefined") { + this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { + return (a !== callback); + }); + } else { + throw Error("Unknown event: " + ev); + } } -}; +} window.HuesWindow = HuesWindow; diff --git a/src/js/ResourceManager.js b/src/js/ResourceManager.js index dbffc7e..ddb7d58 100644 --- a/src/js/ResourceManager.js +++ b/src/js/ResourceManager.js @@ -29,845 +29,847 @@ let TAB_IMAGES = 1; let unique = 0; let getAndIncrementUnique = function() { return unique++; -} +}; // NOTE: Any packs referenced need CORS enabled or loads fail let packsURL = "https://cdn.0x40hu.es/getRespacks.php"; -function Resources(core, huesWin) { - this.core = core; - this.hasUI = false; - - this.resourcePacks = []; - - this.allSongs = []; - this.allImages = []; - this.enabledSongs = []; - this.enabledImages = []; - - this.progressState = []; - this.progressCallback = null; - - // For songs/images - this.listView = null; - this.enabledSongList = null; - this.enabledImageList = null; - this.packView = { - pack: null, - name: null, - creator: null, - size: null, - desc: null, - songCount: null, - imageCount: null, - songList: null, - imageList: null, - packButtons: null, - totalSongs: null, - totalImages: null - }; - this.packsView = { - respackList: null, - remoteList: null, - loadRemote: null, - progressBar: null, - progressStatus: null, - progressCurrent: null, - progressTop: null, - progressPercent: null - }; - this.currentTab = TAB_SONGS; - this.unique = getAndIncrementUnique(); - this.remotes = null; - this.fileInput = null; - this.fileParseQueue = []; - if(core.settings.defaults.enableWindow) { - this.initUI(); - huesWin.addTab("RESOURCES", this.root); +class Resources { + constructor(core, huesWin) { + this.core = core; + this.hasUI = false; + + this.resourcePacks = []; + + this.allSongs = []; + this.allImages = []; + this.enabledSongs = []; + this.enabledImages = []; + + this.progressState = []; + this.progressCallback = null; + + // For songs/images + this.listView = null; + this.enabledSongList = null; + this.enabledImageList = null; + this.packView = { + pack: null, + name: null, + creator: null, + size: null, + desc: null, + songCount: null, + imageCount: null, + songList: null, + imageList: null, + packButtons: null, + totalSongs: null, + totalImages: null + }; + this.packsView = { + respackList: null, + remoteList: null, + loadRemote: null, + progressBar: null, + progressStatus: null, + progressCurrent: null, + progressTop: null, + progressPercent: null + }; + this.currentTab = TAB_SONGS; + this.unique = getAndIncrementUnique(); + this.remotes = null; + this.fileInput = null; + this.fileParseQueue = []; + if(core.settings.defaults.enableWindow) { + this.initUI(); + huesWin.addTab("RESOURCES", this.root); + } } -} -/* Uses HTTP HEAD requests to get the size of all the linked URLs - Returns an Promise.all which will resolve to an array of sizes */ -Resources.prototype.getSizes = function(urls) { - let promises = []; - - urls.forEach(url => { - let p = new Promise((resolve, reject) => { - let xhr = new XMLHttpRequest(); - xhr.open("HEAD", url, true); - xhr.onreadystatechange = function() { - if (this.readyState == this.DONE) { - let bytes = parseInt(xhr.getResponseHeader("Content-Length")); - resolve(bytes / 1024 / 1024); + /* Uses HTTP HEAD requests to get the size of all the linked URLs + Returns an Promise.all which will resolve to an array of sizes */ + getSizes(urls) { + let promises = []; + + urls.forEach(url => { + let p = new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open("HEAD", url, true); + xhr.onreadystatechange = function() { + if (this.readyState == this.DONE) { + let bytes = parseInt(xhr.getResponseHeader("Content-Length")); + resolve(bytes / 1024 / 1024); + } + }; + xhr.onerror = function() { + reject(Error(req.status + ": Could not fetch respack at " + url)); + }; + xhr.send(); + }).catch(error => { + // Infinitely more user friendly than the error Same Origin gives + if(error.code == 1012) { + throw Error("Respack at URL " + url + " is restricted. Check CORS."); + } else { + throw error; } - }; - xhr.onerror = function() { - reject(Error(req.status + ": Could not fetch respack at " + url)); - }; - xhr.send(); - }).catch(error => { - // Infinitely more user friendly than the error Same Origin gives - if(error.code == 1012) { - throw Error("Respack at URL " + url + " is restricted. Check CORS."); - } else { - throw error; - } - }); - promises.push(p); - }); - - return Promise.all(promises); -}; - -// Array of URLs to load, and a callback for when we're done -// Preserves order of URLs being loaded -Resources.prototype.addAll = function(urls, progressCallback) { - if(progressCallback) { - this.progressCallback = progressCallback; - this.progressState = Array.apply(null, Array(urls.length)).map(Number.prototype.valueOf,0); - } - - let respackPromises = []; - - let progressFunc = function(index, progress, pack) { - this.progressState[index] = progress; - this.updateProgress(pack); - }; - - for(let i = 0; i < urls.length; i++) { - let r = new Respack(); - respackPromises.push(r.loadFromURL(urls[i], progressFunc.bind(this, i))); - } - // Start all the promises at once, but add in sequence - return respackPromises.reduce((sequence, packPromise) => { - return sequence.then(() => { - return packPromise; - }).then(pack => { - this.addPack(pack); + }); + promises.push(p); }); - }, Promise.resolve()); -}; + + return Promise.all(promises); + } -Resources.prototype.updateProgress = function(pack) { - let total = 0; - for(let i = 0; i < this.progressState.length; i++) { - total += this.progressState[i]; + // Array of URLs to load, and a callback for when we're done + // Preserves order of URLs being loaded + addAll(urls, progressCallback) { + if(progressCallback) { + this.progressCallback = progressCallback; + this.progressState = Array.apply(null, Array(urls.length)).map(Number.prototype.valueOf,0); + } + + let respackPromises = []; + + let progressFunc = function(index, progress, pack) { + this.progressState[index] = progress; + this.updateProgress(pack); + }; + + for(let i = 0; i < urls.length; i++) { + let r = new Respack(); + respackPromises.push(r.loadFromURL(urls[i], progressFunc.bind(this, i))); + } + // Start all the promises at once, but add in sequence + return respackPromises.reduce((sequence, packPromise) => { + return sequence.then(() => { + return packPromise; + }).then(pack => { + this.addPack(pack); + }); + }, Promise.resolve()); } - total /= this.progressState.length; - this.progressCallback(total, pack); -}; -Resources.prototype.addPack = function(pack) { - console.log("Added", pack.name, "to respacks"); - let id = this.resourcePacks.length; - this.resourcePacks.push(pack); - this.addResourcesToArrays(pack); - this.rebuildEnabled(); - this.updateTotals(); - - let self = this; - this.appendListItem("respacks", pack.name, "res" + id, this.packsView.respackList, - function() { - pack.enabled = this.checked; - self.rebuildEnabled(); - }, function(id) { - this.selectPack(id); - }.bind(this, id) - ); -}; + updateProgress(pack) { + let total = 0; + for(let i = 0; i < this.progressState.length; i++) { + total += this.progressState[i]; + } + total /= this.progressState.length; + this.progressCallback(total, pack); + } -Resources.prototype.addResourcesToArrays = function(pack) { - this.allImages = this.allImages.concat(pack.images); - this.allSongs = this.allSongs.concat(pack.songs); -}; + addPack(pack) { + console.log("Added", pack.name, "to respacks"); + let id = this.resourcePacks.length; + this.resourcePacks.push(pack); + this.addResourcesToArrays(pack); + this.rebuildEnabled(); + this.updateTotals(); -Resources.prototype.rebuildArrays = function() { - this.allSongs = []; - this.allImages = []; - this.allAnimations = []; + let self = this; + this.appendListItem("respacks", pack.name, "res" + id, this.packsView.respackList, + function() { + pack.enabled = this.checked; + self.rebuildEnabled(); + }, function(id) { + this.selectPack(id); + }.bind(this, id) + ); + } - for(let i = 0; i < this.resourcePacks.length; i++) { - this.addResourcesToArrays(this.resourcePacks[i]); + addResourcesToArrays(pack) { + this.allImages = this.allImages.concat(pack.images); + this.allSongs = this.allSongs.concat(pack.songs); } -}; -Resources.prototype.rebuildEnabled = function() { - this.enabledSongs = []; - this.enabledImages = []; + rebuildArrays() { + this.allSongs = []; + this.allImages = []; + this.allAnimations = []; - for(let i = 0; i < this.resourcePacks.length; i++) { - let pack = this.resourcePacks[i]; - if (pack.enabled !== true) { - continue; + for(let i = 0; i < this.resourcePacks.length; i++) { + this.addResourcesToArrays(this.resourcePacks[i]); } - for(let j = 0; j < pack.songs.length; j++) { - let song = pack.songs[j]; - if (song.enabled && this.enabledSongs.indexOf(song) == -1) { - this.enabledSongs.push(song); + } + + rebuildEnabled() { + this.enabledSongs = []; + this.enabledImages = []; + + for(let i = 0; i < this.resourcePacks.length; i++) { + let pack = this.resourcePacks[i]; + if (pack.enabled !== true) { + continue; + } + for(let j = 0; j < pack.songs.length; j++) { + let song = pack.songs[j]; + if (song.enabled && this.enabledSongs.indexOf(song) == -1) { + this.enabledSongs.push(song); + } + } + for(let j = 0; j < pack.images.length; j++) { + let image = pack.images[j]; + if (image.enabled && this.enabledImages.indexOf(image) == -1) { + this.enabledImages.push(image); + } } } - for(let j = 0; j < pack.images.length; j++) { - let image = pack.images[j]; - if (image.enabled && this.enabledImages.indexOf(image) == -1) { - this.enabledImages.push(image); + if(this.hasUI) { + let songList = this.enabledSongList; + while(songList.firstElementChild) { + songList.removeChild(songList.firstElementChild); + } + let imageList = this.enabledImageList; + while(imageList.firstElementChild) { + imageList.removeChild(imageList.firstElementChild); + } + for(let i = 0; i < this.enabledSongs.length; i++) { + let song = this.enabledSongs[i]; + this.appendSimpleListItem(song.title, songList, function(index) { + this.core.setSong(index); + }.bind(this, i)); + } + for(let i = 0; i < this.enabledImages.length; i++) { + let image = this.enabledImages[i]; + this.appendSimpleListItem(image.name, imageList, function(index) { + this.core.setImage(index); + this.core.setIsFullAuto(false); + }.bind(this, i)); } } + this.updateTotals(); } - if(this.hasUI) { - let songList = this.enabledSongList; - while(songList.firstElementChild) { - songList.removeChild(songList.firstElementChild); - } - let imageList = this.enabledImageList; - while(imageList.firstElementChild) { - imageList.removeChild(imageList.firstElementChild); - } - for(let i = 0; i < this.enabledSongs.length; i++) { - let song = this.enabledSongs[i]; - this.appendSimpleListItem(song.title, songList, function(index) { - this.core.setSong(index); - }.bind(this, i)); - } - for(let i = 0; i < this.enabledImages.length; i++) { - let image = this.enabledImages[i]; - this.appendSimpleListItem(image.name, imageList, function(index) { - this.core.setImage(index); - this.core.setIsFullAuto(false); - }.bind(this, i)); + + removePack(pack) { + let index = this.resourcePacks.indexOf(pack); + if (index != -1) { + this.resourcePacks.splice(index, 1); + this.rebuildArrays(); } } - this.updateTotals(); -}; -Resources.prototype.removePack = function(pack) { - let index = this.resourcePacks.indexOf(pack); - if (index != -1) { - this.resourcePacks.splice(index, 1); + removeAllPacks() { + this.resourcePacks = []; this.rebuildArrays(); } -}; -Resources.prototype.removeAllPacks = function() { - this.resourcePacks = []; - this.rebuildArrays(); -}; - -Resources.prototype.getSongNames = function() { - let names = []; - for(let i = 0; i < this.allSongs.length; i++) { - names.push(this.allSongs[i]); + getSongNames() { + let names = []; + for(let i = 0; i < this.allSongs.length; i++) { + names.push(this.allSongs[i]); + } + return names; } - return names; -}; -Resources.prototype.loadLocal = function() { - console.log("Loading local zip(s)"); - - let files = this.fileInput.files; - let p = Promise.resolve(); - for(let i = 0; i < files.length; i++) { - let r = new Respack(); - /*jshint -W083 */ - p = p.then(() => { - return r.loadFromBlob(files[i], (progress, respack) => { - this.localProgress(progress, respack); + loadLocal() { + console.log("Loading local zip(s)"); + + let files = this.fileInput.files; + let p = Promise.resolve(); + for(let i = 0; i < files.length; i++) { + let r = new Respack(); + /*jshint -W083 */ + p = p.then(() => { + return r.loadFromBlob(files[i], (progress, respack) => { + this.localProgress(progress, respack); + }); + }).then(pack => { + this.addPack(pack); + this.localComplete(); }); - }).then(pack => { - this.addPack(pack); - this.localComplete(); + } + return p.then(() => { + console.log("Local respack parsing complete"); }); } - return p.then(() => { - console.log("Local respack parsing complete"); - }); -}; -Resources.prototype.localProgress = function(progress, respack) { - if(!this.hasUI) {return;} - this.packsView.progressStatus.textContent = "Processing..."; + localProgress(progress, respack) { + if(!this.hasUI) {return;} + this.packsView.progressStatus.textContent = "Processing..."; - this.packsView.progressBar.style.width = (progress * 100) + "%"; - this.packsView.progressCurrent.textContent = respack.filesLoaded; - this.packsView.progressTop.textContent = respack.filesToLoad; - this.packsView.progressPercent.textContent = Math.round(progress * 100) + "%"; -}; + this.packsView.progressBar.style.width = (progress * 100) + "%"; + this.packsView.progressCurrent.textContent = respack.filesLoaded; + this.packsView.progressTop.textContent = respack.filesToLoad; + this.packsView.progressPercent.textContent = Math.round(progress * 100) + "%"; + } -Resources.prototype.localComplete = function(progress) { - let progStat = this.packsView.progressStatus; - progStat.textContent = "Complete"; - window.setTimeout(function() {progStat.textContent = "Idle";}, 2000); + localComplete(progress) { + let progStat = this.packsView.progressStatus; + progStat.textContent = "Complete"; + window.setTimeout(function() {progStat.textContent = "Idle";}, 2000); - this.packsView.progressBar.style.width = "100%"; - this.packsView.progressCurrent.textContent = "0b"; - this.packsView.progressTop.textContent = "0b"; - this.packsView.progressPercent.textContent = "0%"; -}; + this.packsView.progressBar.style.width = "100%"; + this.packsView.progressCurrent.textContent = "0b"; + this.packsView.progressTop.textContent = "0b"; + this.packsView.progressPercent.textContent = "0%"; + } -Resources.prototype.initUI = function() { - this.root = document.createElement("div"); - this.root.className = "respacks"; - - let packsContainer = document.createElement("div"); - packsContainer.className = "respacks__manager"; - - let packHeader = document.createElement("div"); - packHeader.textContent = "Current respacks"; - packHeader.className = "respacks__header"; - let packList = document.createElement("div"); - packList.className = "resource-list"; - this.packsView.respackList = packList; - // so we don't use it out of scope in the next if - let remoteHeader = null; - let remoteList = null; - if(!this.core.settings.defaults.disableRemoteResources) { - remoteHeader = document.createElement("div"); - remoteHeader.textContent = "Remote respacks"; - remoteHeader.className = "respacks__header"; - remoteList = document.createElement("div"); - remoteList.className = "resource-list resource-list--fill"; - packList.classList.add("resource-list--fill"); - this.appendSimpleListItem("Click to load the list", remoteList, - this.loadRemotes.bind(this)); - this.packsView.remoteList = remoteList; - } - - let buttons = document.createElement("div"); - buttons.className = "respacks-buttons"; - let loadRemote = document.createElement("div"); - loadRemote.className = "hues-button hidden"; - loadRemote.textContent = "LOAD REMOTE"; - loadRemote.onclick = this.loadCurrentRemote.bind(this); - let loadLocal = document.createElement("div"); - loadLocal.className = "hues-button"; - loadLocal.textContent = "LOAD ZIPS"; - loadLocal.onclick = () => {this.fileInput.click();}; - buttons.appendChild(loadLocal); - buttons.appendChild(loadRemote); - this.packsView.loadRemote = loadRemote; - - this.fileInput = document.createElement("input"); - this.fileInput.type ="file"; - this.fileInput.accept="application/zip"; - this.fileInput.multiple = true; - this.fileInput.onchange = this.loadLocal.bind(this); - - let progressContainer = document.createElement("div"); - progressContainer.className = "progress-container respacks-bottom-container"; - let progressBar = document.createElement("div"); - progressBar.className = "progress-bar"; - let progressFilled = document.createElement("span"); - progressFilled.className = "progress-bar--filled"; - progressBar.appendChild(progressFilled); - let progressStatus = document.createElement("div"); - progressStatus.textContent = "Idle"; - - let progressTexts = document.createElement("div"); - progressTexts.className = "stat-text"; - let progressCurrent = document.createElement("div"); - progressCurrent.textContent = "0b"; - let progressTop = document.createElement("div"); - progressTop.textContent = "0b"; - let progressPercent = document.createElement("div"); - progressPercent.textContent = "0%"; - progressTexts.appendChild(progressCurrent); - progressTexts.appendChild(progressTop); - progressTexts.appendChild(progressPercent); - - this.packsView.progressBar = progressFilled; - this.packsView.progressStatus = progressStatus; - this.packsView.progressCurrent = progressCurrent; - this.packsView.progressTop = progressTop; - this.packsView.progressPercent = progressPercent; - progressContainer.appendChild(progressStatus); - progressContainer.appendChild(progressBar); - progressContainer.appendChild(progressTexts); - - packsContainer.appendChild(packHeader); - packsContainer.appendChild(packList); - if(!this.core.settings.defaults.disableRemoteResources) { - packsContainer.appendChild(remoteHeader); - packsContainer.appendChild(remoteList); - } - packsContainer.appendChild(buttons); - packsContainer.appendChild(progressContainer); - - let indivView = document.createElement("div"); - indivView.className = "respacks__display"; - - let packName = document.createElement("div"); - packName.textContent = ""; + packName.className = "respacks__header"; + let packInfo = document.createElement("div"); + packInfo.className = "stat-text"; + let packCreator = document.createElement("div"); + let packCreatorText = document.createElement("a"); + packCreatorText.className = "unstyled-link"; + packCreatorText.textContent = ""; + packCreator.appendChild(packCreatorText); + packInfo.appendChild(packCreator); + let packSize = document.createElement("div"); + packSize.textContent = "0b"; + packInfo.appendChild(packSize); + let packDesc = document.createElement("div"); + packDesc.className = "respack-description"; + packDesc.textContent = ""; - this.currentTab = TAB_SONGS; - }; - - imageCount.onclick = () => { - imageCount.classList.add("respack-tab--checked"); - songCount.classList.remove("respack-tab--checked"); + let tabContainer = document.createElement("div"); + tabContainer.className = "respack-tab-container"; - imageList.classList.add("respack-tab__content--checked"); - songList.classList.remove("respack-tab__content--checked"); + let songCount = document.createElement("div"); + songCount.textContent = "Songs:"; + songCount.className = "respack-tab respack-tab--checked"; + + let imageCount = document.createElement("div"); + imageCount.textContent = "Images:"; + imageCount.className = "respack-tab"; + + let songList = document.createElement("div"); + songList.className = "resource-list respack-tab__content respack-tab__content--checked"; + let imageList = document.createElement("div"); + imageList.className = "resource-list respack-tab__content"; - this.currentTab = TAB_IMAGES; - }; - - let packButtons = document.createElement("div"); - packButtons.className = "respacks-buttons respacks-buttons--fill invisible"; - let enableAll = document.createElement("div"); - enableAll.textContent = "ENABLE ALL"; - enableAll.className = "hues-button"; - enableAll.onclick = this.enableAll.bind(this); - let invert = document.createElement("div"); - invert.textContent = "INVERT"; - invert.className = "hues-button"; - invert.onclick = this.invert.bind(this); - let disableAll = document.createElement("div"); - disableAll.textContent = "DISABLE ALL"; - disableAll.className = "hues-button"; - disableAll.onclick = this.disableAll.bind(this); - packButtons.appendChild(enableAll); - packButtons.appendChild(invert); - packButtons.appendChild(disableAll); - - let totalCounts = document.createElement("div"); - totalCounts.className = "respacks-bottom-container"; - - let totalSongsCont = document.createElement("div"); - totalSongsCont.className = "respacks-count-container"; - let totalSongsLabel = document.createElement("span"); - totalSongsLabel.textContent = "Total Songs:"; - let totalSongs = document.createElement("span"); - totalSongs.className = "respacks-counts"; - totalSongsCont.appendChild(totalSongsLabel); - totalSongsCont.appendChild(totalSongs); - - let totalImagesCont = document.createElement("div"); - totalImagesCont.className = "respacks-count-container"; - let totalImagesLabel = document.createElement("span"); - totalImagesLabel.textContent = "Total images:"; - let totalImages = document.createElement("span"); - totalImages.className = "respacks-counts"; - totalImagesCont.appendChild(totalImagesLabel); - totalImagesCont.appendChild(totalImages); - - totalCounts.appendChild(totalSongsCont); - totalCounts.appendChild(totalImagesCont); - - this.packView.name = packName; - this.packView.creator = packCreatorText; - this.packView.size = packSize; - this.packView.desc = packDesc; - this.packView.songCount = songCount; - this.packView.imageCount = imageCount; - this.packView.songList = songList; - this.packView.imageList = imageList; - this.packView.packButtons = packButtons; - this.packView.totalSongs = totalSongs; - this.packView.totalImages = totalImages; - - indivView.appendChild(packName); - indivView.appendChild(packInfo); - indivView.appendChild(packDesc); - - tabContainer.appendChild(songCount); - tabContainer.appendChild(imageCount); - indivView.appendChild(tabContainer); - indivView.appendChild(songList); - indivView.appendChild(imageList); - - indivView.appendChild(packButtons); - indivView.appendChild(totalCounts); - - this.root.appendChild(packsContainer); - this.root.appendChild(indivView); - - this.listView = document.createElement("div"); - this.enabledSongList = document.createElement("div"); - this.enabledSongList.className = "resource-list respacks-enabledsongs hidden"; - this.enabledImageList = document.createElement("div"); - this.enabledImageList.className = "resource-list respacks-enabledimages hidden"; - - this.listView.appendChild(this.enabledSongList); - this.listView.appendChild(this.enabledImageList); - - this.hasUI = true; -}; - -Resources.prototype.hideLists = function() { - if(!this.hasUI) {return;} - this.enabledSongList.classList.add("hidden"); - this.enabledImageList.classList.add("hidden"); -}; - -Resources.prototype.toggleVisible = function(me, other) { - if(!this.hasUI) {return;} - if(me.classList.contains("hidden")) { - me.classList.remove("hidden"); - } else { - me.classList.add("hidden"); - } - other.classList.add("hidden"); -}; - -Resources.prototype.toggleSongList = function() { - this.toggleVisible(this.enabledSongList, this.enabledImageList); -}; - -Resources.prototype.toggleImageList = function() { - this.toggleVisible(this.enabledImageList, this.enabledSongList); -}; - -Resources.prototype.updateTotals = function() { - if(!this.hasUI) {return;} - this.packView.totalSongs.textContent = - this.enabledSongs.length + "/" + this.allSongs.length; - this.packView.totalImages.textContent = - this.enabledImages.length + "/" + this.allImages.length; -}; - -Resources.prototype.truncateNum = function(num) { - return Math.round(num * 100) / 100; -}; - -Resources.prototype.selectPack = function(id) { - let pack = this.resourcePacks[id]; - this.packView.pack = pack; + songCount.onclick = () => { + songCount.classList.add("respack-tab--checked"); + imageCount.classList.remove("respack-tab--checked"); + + songList.classList.add("respack-tab__content--checked"); + imageList.classList.remove("respack-tab__content--checked"); + + this.currentTab = TAB_SONGS; + }; + + imageCount.onclick = () => { + imageCount.classList.add("respack-tab--checked"); + songCount.classList.remove("respack-tab--checked"); + + imageList.classList.add("respack-tab__content--checked"); + songList.classList.remove("respack-tab__content--checked"); + + this.currentTab = TAB_IMAGES; + }; + + let packButtons = document.createElement("div"); + packButtons.className = "respacks-buttons respacks-buttons--fill invisible"; + let enableAll = document.createElement("div"); + enableAll.textContent = "ENABLE ALL"; + enableAll.className = "hues-button"; + enableAll.onclick = this.enableAll.bind(this); + let invert = document.createElement("div"); + invert.textContent = "INVERT"; + invert.className = "hues-button"; + invert.onclick = this.invert.bind(this); + let disableAll = document.createElement("div"); + disableAll.textContent = "DISABLE ALL"; + disableAll.className = "hues-button"; + disableAll.onclick = this.disableAll.bind(this); + packButtons.appendChild(enableAll); + packButtons.appendChild(invert); + packButtons.appendChild(disableAll); + + let totalCounts = document.createElement("div"); + totalCounts.className = "respacks-bottom-container"; + + let totalSongsCont = document.createElement("div"); + totalSongsCont.className = "respacks-count-container"; + let totalSongsLabel = document.createElement("span"); + totalSongsLabel.textContent = "Total Songs:"; + let totalSongs = document.createElement("span"); + totalSongs.className = "respacks-counts"; + totalSongsCont.appendChild(totalSongsLabel); + totalSongsCont.appendChild(totalSongs); + + let totalImagesCont = document.createElement("div"); + totalImagesCont.className = "respacks-count-container"; + let totalImagesLabel = document.createElement("span"); + totalImagesLabel.textContent = "Total images:"; + let totalImages = document.createElement("span"); + totalImages.className = "respacks-counts"; + totalImagesCont.appendChild(totalImagesLabel); + totalImagesCont.appendChild(totalImages); + + totalCounts.appendChild(totalSongsCont); + totalCounts.appendChild(totalImagesCont); + + this.packView.name = packName; + this.packView.creator = packCreatorText; + this.packView.size = packSize; + this.packView.desc = packDesc; + this.packView.songCount = songCount; + this.packView.imageCount = imageCount; + this.packView.songList = songList; + this.packView.imageList = imageList; + this.packView.packButtons = packButtons; + this.packView.totalSongs = totalSongs; + this.packView.totalImages = totalImages; + + indivView.appendChild(packName); + indivView.appendChild(packInfo); + indivView.appendChild(packDesc); + + tabContainer.appendChild(songCount); + tabContainer.appendChild(imageCount); + indivView.appendChild(tabContainer); + indivView.appendChild(songList); + indivView.appendChild(imageList); + + indivView.appendChild(packButtons); + indivView.appendChild(totalCounts); - this.packView.packButtons.classList.remove("invisible"); - this.packsView.loadRemote.classList.add("hidden"); + this.root.appendChild(packsContainer); + this.root.appendChild(indivView); - this.packView.name.textContent = pack.name; - this.packView.creator.textContent = pack.author; - this.packView.creator.href = pack.link ? pack.link : ""; - let size = pack.size / 1024; - if(size < 512) { - this.packView.size.textContent = this.truncateNum(size) + "kB"; - } else { - this.packView.size.textContent = this.truncateNum(size / 1024) + "MB"; - } - this.packView.desc.textContent = pack.description; - this.packView.songCount.textContent = "Songs: " + pack.songs.length; - this.packView.imageCount.textContent = "Images: " + pack.images.length; + this.listView = document.createElement("div"); + this.enabledSongList = document.createElement("div"); + this.enabledSongList.className = "resource-list respacks-enabledsongs hidden"; + this.enabledImageList = document.createElement("div"); + this.enabledImageList.className = "resource-list respacks-enabledimages hidden"; - let songList = this.packView.songList; - let imageList = this.packView.imageList; - while (songList.firstElementChild) { - songList.removeChild(songList.firstElementChild); - } - while (imageList.firstElementChild) { - imageList.removeChild(imageList.firstElementChild); + this.listView.appendChild(this.enabledSongList); + this.listView.appendChild(this.enabledImageList); + + this.hasUI = true; } - for(let i = 0; i < pack.songs.length; i++) { - let song = pack.songs[i]; - this.appendListItem("songs", song.title, "song" + i, songList, - this.selectResourceCallback(song), - this.clickResourceCallback.bind(this, song, true), - song.enabled); + hideLists() { + if(!this.hasUI) {return;} + this.enabledSongList.classList.add("hidden"); + this.enabledImageList.classList.add("hidden"); } - for(let i = 0; i < pack.images.length; i++) { - let image = pack.images[i]; - this.appendListItem("images", image.name, "image" + i, imageList, - this.selectResourceCallback(image), - this.clickResourceCallback.bind(this, image, false), - image.enabled); + toggleVisible(me, other) { + if(!this.hasUI) {return;} + if(me.classList.contains("hidden")) { + me.classList.remove("hidden"); + } else { + me.classList.add("hidden"); + } + other.classList.add("hidden"); } -}; - -Resources.prototype.selectResourceCallback = function(res) { - let self = this; - return function() { - res.enabled = this.checked; - self.rebuildEnabled(); - }; -}; -Resources.prototype.clickResourceCallback = function(res, isSong) { - if(!res.enabled) { - res.enabled = true; - this.rebuildEnabled(); - // rebuild display - this.selectPack(this.resourcePacks.indexOf(this.packView.pack)); - } - if(isSong) { - this.core.setSong(this.enabledSongs.indexOf(res)); - } else { - this.core.setImage(this.enabledImages.indexOf(res)); - this.core.setIsFullAuto(false); + toggleSongList() { + this.toggleVisible(this.enabledSongList, this.enabledImageList); } -}; -Resources.prototype.getEnabledTabContents = function() { - let pack = this.packView.pack; - if(!pack) { - return null; + toggleImageList() { + this.toggleVisible(this.enabledImageList, this.enabledSongList); } - if(this.currentTab == TAB_SONGS) { - return {arr: pack.songs, - elName: "song"}; - } else { - return {arr: pack.images, - elName: "image"}; - } -}; -Resources.prototype.enableAll = function() { - let tab = this.getEnabledTabContents(); - if(!tab) - return; - for(let i = 0; i < tab.arr.length; i++) { - tab.arr[i].enabled = true; - document.getElementById(tab.elName + i + "-" + this.unique).checked = true; + updateTotals() { + if(!this.hasUI) {return;} + this.packView.totalSongs.textContent = + this.enabledSongs.length + "/" + this.allSongs.length; + this.packView.totalImages.textContent = + this.enabledImages.length + "/" + this.allImages.length; } - this.rebuildEnabled(); -}; -Resources.prototype.disableAll = function() { - let tab = this.getEnabledTabContents(); - if(!tab) - return; - for(let i = 0; i < tab.arr.length; i++) { - tab.arr[i].enabled = false; - document.getElementById(tab.elName + i + "-" + this.unique).checked = false; + truncateNum(num) { + return Math.round(num * 100) / 100; } - this.rebuildEnabled(); -}; -Resources.prototype.invert = function() { - let tab = this.getEnabledTabContents(); - if(!tab) - return; - for(let i = 0; i < tab.arr.length; i++) { - tab.arr[i].enabled = !tab.arr[i].enabled; - document.getElementById(tab.elName + i + "-" + this.unique).checked = tab.arr[i].enabled; - } - this.rebuildEnabled(); -}; + selectPack(id) { + let pack = this.resourcePacks[id]; + this.packView.pack = pack; -Resources.prototype.appendListItem = function(name, value, id, root, oncheck, onclick, checked) { - if(!this.hasUI) {return;} - if(checked === undefined) { - checked = true; - } - let div = document.createElement("div"); - div.className = "respacks-listitem"; - let checkbox = document.createElement("input"); - checkbox.type = "checkbox"; - checkbox.name = name; - checkbox.value = value; - checkbox.id = id + "-" + this.unique; - checkbox.checked = checked; - checkbox.onclick = oncheck; - let checkStyler = document.createElement("label"); - checkStyler.htmlFor = checkbox.id; - let label = document.createElement("span"); - label.textContent = value; - label.onclick = onclick; - div.appendChild(checkbox); - div.appendChild(checkStyler); - div.appendChild(label); - root.appendChild(div); -}; + this.packView.packButtons.classList.remove("invisible"); + this.packsView.loadRemote.classList.add("hidden"); -Resources.prototype.loadRemotes = function() { - let remoteList = this.packsView.remoteList; - while(remoteList.firstElementChild) { - remoteList.removeChild(remoteList.firstElementChild); - } - let item = this.appendSimpleListItem("Loading...", remoteList); - - let req = new XMLHttpRequest(); - req.open('GET', packsURL, true); - req.responseType = 'json'; - req.onload = () => { - if(!req.response) { - req.onerror(); - } - this.remotes = req.response; - this.populateRemotes(); - }; - req.onerror = () => { - item.textContent = "Could not load list! Click to try again"; - item.onclick = this.loadRemotes.bind(this); - }; - req.send(); -}; + this.packView.name.textContent = pack.name; + this.packView.creator.textContent = pack.author; + this.packView.creator.href = pack.link ? pack.link : ""; + let size = pack.size / 1024; + if(size < 512) { + this.packView.size.textContent = this.truncateNum(size) + "kB"; + } else { + this.packView.size.textContent = this.truncateNum(size / 1024) + "MB"; + } + this.packView.desc.textContent = pack.description; + this.packView.songCount.textContent = "Songs: " + pack.songs.length; + this.packView.imageCount.textContent = "Images: " + pack.images.length; -Resources.prototype.populateRemotes = function() { - let remoteList = this.packsView.remoteList; - while(remoteList.firstElementChild) { - remoteList.removeChild(remoteList.firstElementChild); - } - for(let i = 0; i < this.remotes.length; i++) { - this.remotes[i].loaded = false; - this.appendSimpleListItem(this.remotes[i].name, remoteList, - function(index) { - this.selectRemotePack(index); - }.bind(this, i)); - } -}; + let songList = this.packView.songList; + let imageList = this.packView.imageList; + while (songList.firstElementChild) { + songList.removeChild(songList.firstElementChild); + } + while (imageList.firstElementChild) { + imageList.removeChild(imageList.firstElementChild); + } -Resources.prototype.selectRemotePack = function(id) { - let pack = this.remotes[id]; - this.packView.pack = pack; + for(let i = 0; i < pack.songs.length; i++) { + let song = pack.songs[i]; + this.appendListItem("songs", song.title, "song" + i, songList, + this.selectResourceCallback(song), + this.clickResourceCallback.bind(this, song, true), + song.enabled); + } - this.packView.packButtons.classList.add("invisible"); - this.packsView.loadRemote.classList.remove("hidden"); - if(pack.loaded) { - this.packsView.loadRemote.classList.add("hues-button--loaded"); - this.packsView.loadRemote.textContent = "LOADED"; - } else { - this.packsView.loadRemote.classList.remove("hues-button--loaded"); - this.packsView.loadRemote.textContent = "LOAD REMOTE"; + for(let i = 0; i < pack.images.length; i++) { + let image = pack.images[i]; + this.appendListItem("images", image.name, "image" + i, imageList, + this.selectResourceCallback(image), + this.clickResourceCallback.bind(this, image, false), + image.enabled); + } } - this.packView.name.textContent = pack.name; - this.packView.creator.textContent = pack.author; - this.packView.creator.href = pack.link ? pack.link : ""; - let size = pack.size / 1024; - if(size < 512) { - this.packView.size.textContent = this.truncateNum(size) + "kB"; - } else { - this.packView.size.textContent = this.truncateNum(size / 1024) + "MB"; + selectResourceCallback(res) { + let self = this; + return function() { + res.enabled = this.checked; + self.rebuildEnabled(); + }; } - this.packView.desc.textContent = pack.description; - this.packView.songCount.textContent = "Songs: " + pack.songcount; - this.packView.imageCount.textContent = "Images: " + pack.imagecount; - let songList = this.packView.songList; - let imageList = this.packView.imageList; - while (songList.firstElementChild) { - songList.removeChild(songList.firstElementChild); + clickResourceCallback(res, isSong) { + if(!res.enabled) { + res.enabled = true; + this.rebuildEnabled(); + // rebuild display + this.selectPack(this.resourcePacks.indexOf(this.packView.pack)); + } + if(isSong) { + this.core.setSong(this.enabledSongs.indexOf(res)); + } else { + this.core.setImage(this.enabledImages.indexOf(res)); + this.core.setIsFullAuto(false); + } } - while (imageList.firstElementChild) { - imageList.removeChild(imageList.firstElementChild); + + getEnabledTabContents() { + let pack = this.packView.pack; + if(!pack) { + return null; + } + if(this.currentTab == TAB_SONGS) { + return {arr: pack.songs, + elName: "song"}; + } else { + return {arr: pack.images, + elName: "image"}; + } } - for(let i = 0; i < pack.songs.length; i++) { - let song = pack.songs[i]; - this.appendSimpleListItem(song, songList); + enableAll() { + let tab = this.getEnabledTabContents(); + if(!tab) + return; + for(let i = 0; i < tab.arr.length; i++) { + tab.arr[i].enabled = true; + document.getElementById(tab.elName + i + "-" + this.unique).checked = true; + } + this.rebuildEnabled(); } - let moreSongs = pack.songcount - pack.songs.length; - if(moreSongs > 0) { - let text = "... and " + moreSongs + " more song"; - if(moreSongs > 1) { - text += "s"; + + disableAll() { + let tab = this.getEnabledTabContents(); + if(!tab) + return; + for(let i = 0; i < tab.arr.length; i++) { + tab.arr[i].enabled = false; + document.getElementById(tab.elName + i + "-" + this.unique).checked = false; } - this.appendSimpleListItem(text + ".", songList); - this.appendSimpleListItem("Load the respack to show the rest!", songList); + this.rebuildEnabled(); } - for(let i = 0; i < pack.images.length; i++) { - let image = pack.images[i]; - this.appendSimpleListItem(image, imageList); + invert() { + let tab = this.getEnabledTabContents(); + if(!tab) + return; + for(let i = 0; i < tab.arr.length; i++) { + tab.arr[i].enabled = !tab.arr[i].enabled; + document.getElementById(tab.elName + i + "-" + this.unique).checked = tab.arr[i].enabled; + } + this.rebuildEnabled(); } - let moreImages = pack.imagecount - pack.images.length; - if(moreImages > 0) { - let text = "... and " + moreImages + " more image"; - if(moreImages > 1) { - text += "s"; + + appendListItem(name, value, id, root, oncheck, onclick, checked) { + if(!this.hasUI) {return;} + if(checked === undefined) { + checked = true; + } + let div = document.createElement("div"); + div.className = "respacks-listitem"; + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.name = name; + checkbox.value = value; + checkbox.id = id + "-" + this.unique; + checkbox.checked = checked; + checkbox.onclick = oncheck; + let checkStyler = document.createElement("label"); + checkStyler.htmlFor = checkbox.id; + let label = document.createElement("span"); + label.textContent = value; + label.onclick = onclick; + div.appendChild(checkbox); + div.appendChild(checkStyler); + div.appendChild(label); + root.appendChild(div); + } + + loadRemotes() { + let remoteList = this.packsView.remoteList; + while(remoteList.firstElementChild) { + remoteList.removeChild(remoteList.firstElementChild); + } + let item = this.appendSimpleListItem("Loading...", remoteList); + + let req = new XMLHttpRequest(); + req.open('GET', packsURL, true); + req.responseType = 'json'; + req.onload = () => { + if(!req.response) { + req.onerror(); + } + this.remotes = req.response; + this.populateRemotes(); + }; + req.onerror = () => { + item.textContent = "Could not load list! Click to try again"; + item.onclick = this.loadRemotes.bind(this); + }; + req.send(); + } + + populateRemotes() { + let remoteList = this.packsView.remoteList; + while(remoteList.firstElementChild) { + remoteList.removeChild(remoteList.firstElementChild); + } + for(let i = 0; i < this.remotes.length; i++) { + this.remotes[i].loaded = false; + this.appendSimpleListItem(this.remotes[i].name, remoteList, + function(index) { + this.selectRemotePack(index); + }.bind(this, i)); } - this.appendSimpleListItem(text + ".", imageList); - this.appendSimpleListItem("Load the respack to show the rest!", imageList); } -}; -Resources.prototype.loadCurrentRemote = function() { - let pack = this.packView.pack; + selectRemotePack(id) { + let pack = this.remotes[id]; + this.packView.pack = pack; + + this.packView.packButtons.classList.add("invisible"); + this.packsView.loadRemote.classList.remove("hidden"); + if(pack.loaded) { + this.packsView.loadRemote.classList.add("hues-button--loaded"); + this.packsView.loadRemote.textContent = "LOADED"; + } else { + this.packsView.loadRemote.classList.remove("hues-button--loaded"); + this.packsView.loadRemote.textContent = "LOAD REMOTE"; + } + + this.packView.name.textContent = pack.name; + this.packView.creator.textContent = pack.author; + this.packView.creator.href = pack.link ? pack.link : ""; + let size = pack.size / 1024; + if(size < 512) { + this.packView.size.textContent = this.truncateNum(size) + "kB"; + } else { + this.packView.size.textContent = this.truncateNum(size / 1024) + "MB"; + } + this.packView.desc.textContent = pack.description; + this.packView.songCount.textContent = "Songs: " + pack.songcount; + this.packView.imageCount.textContent = "Images: " + pack.imagecount; + + let songList = this.packView.songList; + let imageList = this.packView.imageList; + while (songList.firstElementChild) { + songList.removeChild(songList.firstElementChild); + } + while (imageList.firstElementChild) { + imageList.removeChild(imageList.firstElementChild); + } + + for(let i = 0; i < pack.songs.length; i++) { + let song = pack.songs[i]; + this.appendSimpleListItem(song, songList); + } + let moreSongs = pack.songcount - pack.songs.length; + if(moreSongs > 0) { + let text = "... and " + moreSongs + " more song"; + if(moreSongs > 1) { + text += "s"; + } + this.appendSimpleListItem(text + ".", songList); + this.appendSimpleListItem("Load the respack to show the rest!", songList); + } - // Not actually a remote, ignore. How did you press this :< - if(pack.loaded === undefined || pack.loaded) { - return; + for(let i = 0; i < pack.images.length; i++) { + let image = pack.images[i]; + this.appendSimpleListItem(image, imageList); + } + let moreImages = pack.imagecount - pack.images.length; + if(moreImages > 0) { + let text = "... and " + moreImages + " more image"; + if(moreImages > 1) { + text += "s"; + } + this.appendSimpleListItem(text + ".", imageList); + this.appendSimpleListItem("Load the respack to show the rest!", imageList); + } } - // TODO Error checking on failure - pack.loaded = true; - this.packsView.loadRemote.className = "hues-button hues-button--loaded"; - this.packsView.loadRemote.textContent = "LOADING"; - this.addAll([pack.url], (progress, respack) => { - this.remoteProgress(progress, respack); + loadCurrentRemote() { + let pack = this.packView.pack; + + // Not actually a remote, ignore. How did you press this :< + if(pack.loaded === undefined || pack.loaded) { + return; } - ).then(this.remoteComplete.bind(this)); -}; -Resources.prototype.remoteProgress = function(progress, respack) { - if(progress < 0.5) { - this.packsView.progressStatus.textContent = "Downloading..."; - this.packsView.progressCurrent.textContent = Math.round(respack.downloaded / 1024) + "b"; - this.packsView.progressTop.textContent = Math.round(respack.size / 1024) + "b"; - this.packsView.progressBar.style.width = (progress * 2 * 100) + "%"; - this.packsView.progressPercent.textContent = Math.round(progress * 2 * 100) + "%"; - } else { - this.packsView.progressStatus.textContent = "Processing..."; - this.packsView.progressCurrent.textContent = respack.filesLoaded; - this.packsView.progressTop.textContent = respack.filesToLoad; - this.packsView.progressBar.style.width = ((progress - 0.5) * 2 * 100) + "%"; - this.packsView.progressPercent.textContent = Math.round((progress - 0.5) * 2 * 100) + "%"; + // TODO Error checking on failure + pack.loaded = true; + this.packsView.loadRemote.className = "hues-button hues-button--loaded"; + this.packsView.loadRemote.textContent = "LOADING"; + this.addAll([pack.url], (progress, respack) => { + this.remoteProgress(progress, respack); + } + ).then(this.remoteComplete.bind(this)); + } + + remoteProgress(progress, respack) { + if(progress < 0.5) { + this.packsView.progressStatus.textContent = "Downloading..."; + this.packsView.progressCurrent.textContent = Math.round(respack.downloaded / 1024) + "b"; + this.packsView.progressTop.textContent = Math.round(respack.size / 1024) + "b"; + this.packsView.progressBar.style.width = (progress * 2 * 100) + "%"; + this.packsView.progressPercent.textContent = Math.round(progress * 2 * 100) + "%"; + } else { + this.packsView.progressStatus.textContent = "Processing..."; + this.packsView.progressCurrent.textContent = respack.filesLoaded; + this.packsView.progressTop.textContent = respack.filesToLoad; + this.packsView.progressBar.style.width = ((progress - 0.5) * 2 * 100) + "%"; + this.packsView.progressPercent.textContent = Math.round((progress - 0.5) * 2 * 100) + "%"; + } } -}; -Resources.prototype.remoteComplete = function() { - let progStat = this.packsView.progressStatus; - progStat.textContent = "Complete"; - window.setTimeout(function() {progStat.textContent = "Idle";}, 2000); - this.packsView.loadRemote.textContent = "LOADED"; + remoteComplete() { + let progStat = this.packsView.progressStatus; + progStat.textContent = "Complete"; + window.setTimeout(function() {progStat.textContent = "Idle";}, 2000); + this.packsView.loadRemote.textContent = "LOADED"; - this.packsView.progressBar.style.width = "100%"; - this.packsView.progressCurrent.textContent = "0b"; - this.packsView.progressTop.textContent = "0b"; - this.packsView.progressPercent.textContent = "0%"; -}; + this.packsView.progressBar.style.width = "100%"; + this.packsView.progressCurrent.textContent = "0b"; + this.packsView.progressTop.textContent = "0b"; + this.packsView.progressPercent.textContent = "0%"; + } -Resources.prototype.appendSimpleListItem = function(value, root, onclick) { - let div = document.createElement("div"); - div.className = "respacks-listitem"; - let label = document.createElement("span"); - // Because we're using textContent, we replace with literal & - label.textContent = value.replace(/&/g, '&'); - label.onclick = onclick; - div.appendChild(label); - root.appendChild(div); - return label; -}; + appendSimpleListItem(value, root, onclick) { + let div = document.createElement("div"); + div.className = "respacks-listitem"; + let label = document.createElement("span"); + // Because we're using textContent, we replace with literal & + label.textContent = value.replace(/&/g, '&'); + label.onclick = onclick; + div.appendChild(label); + root.appendChild(div); + return label; + } +} window.Resources = Resources; diff --git a/src/js/ResourcePack.js b/src/js/ResourcePack.js index 3036268..1e29fbb 100644 --- a/src/js/ResourcePack.js +++ b/src/js/ResourcePack.js @@ -22,513 +22,514 @@ (function(window, document) { "use strict"; -let debugConsole = false; +const debugConsole = false; function debug() { if(debugConsole) { console.log.apply(window.console, arguments); } } -function Respack() { - this.songs = []; - this.songQueue = []; - this.images = []; - this.imageQueue = []; +const audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i"); +const imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i"); +const animRegex = new RegExp("(.*?)_\\d+$"); - this.name = ""; - this.author = ""; - this.description = ""; - this.link = null; +class Respack { + constructor() { + this.songs = []; + this.songQueue = []; + this.images = []; + this.imageQueue = []; - this.size = -1; - this.downloaded = -1; - this.enabled = true; + this.name = ""; + this.author = ""; + this.description = ""; + this.link = null; - this._xmlQueue = []; + this.size = -1; + this.downloaded = -1; + this.enabled = true; - this.totalFiles = -1; - - // For zip parsing progress events - this.progressCallback = null; - this.filesToLoad = 0; - this.filesLoaded = 0; - this.loadedFromURL = false; -} + this._xmlQueue = []; -Respack.prototype.audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i"); -Respack.prototype.imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i"); -Respack.prototype.animRegex = new RegExp("(.*?)_\\d+$"); + this.totalFiles = -1; -Respack.prototype.updateProgress = function(override) { - if(this.progressCallback) { - let percent = this.filesLoaded / this.filesToLoad; - if(this.loadedFromURL) { - percent = (percent / 2) + 0.5; - } - this.progressCallback(typeof override === "number" ? override : percent, this); + // For zip parsing progress events + this.progressCallback = null; + this.filesToLoad = 0; + this.filesLoaded = 0; + this.loadedFromURL = false; } -}; -Respack.prototype.loadFromURL = function(url, progress) { - this.loadedFromURL = true; - if(progress) { - this.progressCallback = progress; + updateProgress(override) { + if(this.progressCallback) { + let percent = this.filesLoaded / this.filesToLoad; + if(this.loadedFromURL) { + percent = (percent / 2) + 0.5; + } + this.progressCallback(typeof override === "number" ? override : percent, this); + } } - return this.getBlob(url) - .then(response => { - return this.loadFromBlob(response); - }); -}; + loadFromURL(url, progress) { + this.loadedFromURL = true; + if(progress) { + this.progressCallback = progress; + } -Respack.prototype.getBlob = function(url, progress) { - if(progress) { - this.progressCallback = progress; + return this.getBlob(url) + .then(response => { + return this.loadFromBlob(response); + }); } - return new Promise ((resolve, reject) => { - let req = new XMLHttpRequest(); - req.open('GET', url, true); - req.responseType = 'blob'; - req.onload = () => { - if(req.status == 200) { - resolve(req.response); - } else { + + getBlob(url, progress) { + if(progress) { + this.progressCallback = progress; + } + return new Promise ((resolve, reject) => { + let req = new XMLHttpRequest(); + req.open('GET', url, true); + req.responseType = 'blob'; + req.onload = () => { + if(req.status == 200) { + resolve(req.response); + } else { + reject(Error(req.status + ": Could not fetch respack at " + url)); + } + }; + req.onerror = function() { reject(Error(req.status + ": Could not fetch respack at " + url)); - } - }; - req.onerror = function() { - reject(Error(req.status + ": Could not fetch respack at " + url)); - }; - req.onprogress = event => { - if (event.lengthComputable) { - this.size = event.total; - this.downloaded = event.loaded; - let percent = event.loaded / event.total; - if(this.progressCallback) { - this.progressCallback(percent / 2, this); // because of processing too + }; + req.onprogress = event => { + if (event.lengthComputable) { + this.size = event.total; + this.downloaded = event.loaded; + let percent = event.loaded / event.total; + if(this.progressCallback) { + this.progressCallback(percent / 2, this); // because of processing too + } } + }; + req.send(); + }).catch(error => { + // Infinitely more user friendly than the error Same Origin gives + if(error.code == 1012) { + throw Error("Respack at URL " + url + " is restricted. Check CORS."); + } else { + throw error; } - }; - req.send(); - }).catch(error => { - // Infinitely more user friendly than the error Same Origin gives - if(error.code == 1012) { - throw Error("Respack at URL " + url + " is restricted. Check CORS."); - } else { - throw error; - } - }); -}; + }); + } -Respack.prototype.loadFromBlob = function(blob, progress) { - if(progress) { - this.progressCallback = progress; + loadFromBlob(blob, progress) { + if(progress) { + this.progressCallback = progress; + } + // We don't get progress events for loading the zip, set 0 progress + this.updateProgress(this.loadedFromURL ? 0.5 : 0); + return new Promise((resolve, reject) => { + this.size = blob.size; + let file = new zip.fs.FS(); + file.importBlob(blob, + () => { + resolve(file); + }, + error => { // failure + reject(Error("Respack error:", error.toString())); + } + ); + }).then(zip => { + return this.parseZip(zip); + }).then(() => { + return this; + }); } - // We don't get progress events for loading the zip, set 0 progress - this.updateProgress(this.loadedFromURL ? 0.5 : 0); - return new Promise((resolve, reject) => { - this.size = blob.size; - let file = new zip.fs.FS(); - file.importBlob(blob, - () => { - resolve(file); - }, - error => { // failure - reject(Error("Respack error:", error.toString())); - } - ); - }).then(zip => { - return this.parseZip(zip); - }).then(() => { - return this; - }); -}; -Respack.prototype.parseZip = function(zip) { - let entries = zip.entries; + parseZip(zip) { + let entries = zip.entries; - this.totalFiles = 0; - // Progress events - this.filesToLoad = 0; - this.filesLoaded = 0; + this.totalFiles = 0; + // Progress events + this.filesToLoad = 0; + this.filesLoaded = 0; - // Get everything started - for(let i = 0; i < entries.length; i++) { - if(!entries[i].directory && entries[i].name) { - this.totalFiles++; - this.parseFile(entries[i]); + // Get everything started + for(let i = 0; i < entries.length; i++) { + if(!entries[i].directory && entries[i].name) { + this.totalFiles++; + this.parseFile(entries[i]); + } } + + return this.parseSongQueue() + .then(() => { + return this.parseImageQueue(); + }).then(() => { + return this.parseXML(); + }).then(() => { + // Cleanup + this._xmlQueue = []; + console.log("Loaded", this.name, "successfully with", this.songs.length, + "songs and", this.images.length, "images."); + }); } - return this.parseSongQueue() - .then(() => { - return this.parseImageQueue(); - }).then(() => { - return this.parseXML(); - }).then(() => { - // Cleanup - this._xmlQueue = []; - console.log("Loaded", this.name, "successfully with", this.songs.length, - "songs and", this.images.length, "images."); - }); -}; + parseFile(file) { + let name = file.name; + if (name.match(audioExtensions)) { + this.songQueue.push(this.parseSong(file)); + this.filesToLoad++; + } else if (name.match(imageExtensions)) { + this.imageQueue.push(this.parseImage(file)); + this.filesToLoad++; + } else if(name.toLowerCase().endsWith(".xml")){ + this._xmlQueue.push(this.loadXML(file)); + } + } + + parseSong(file) { + let name = file.name.replace(audioExtensions, ""); + debug("parsing song: " + name); + if (this.containsSong(name)) { + let oldSong = this.getSong(name); + debug("WARNING: Song", name, "already exists! Conflict with", name, "and", oldSong.name); + } else { + let newSong = {"name":name, + "title":null, + "rhythm":null, + "source":null, + //"crc":this.quickCRC(file), TODO + "sound":null, + "enabled":true, + "filename":file.name, + "charsPerBeat": null}; + let extension = file.name.split('.').pop().toLowerCase(); + let mime = ""; + switch(extension) { + case "mp3": + mime = "audio/mpeg3"; + break; + case "ogg": + mime = "audio/ogg"; + break; + case "wav": + mime = "audio/wav"; + break; + default: + mime = "application/octet-stream"; + } + this.songs.push(newSong); + return new Promise((resolve, reject) => { + file.getBlob(mime, sound => { + resolve(sound); + }); + }).then(blob => { + return new Promise((resolve, reject) => { + // Because blobs are crap + let fr = new FileReader(); + fr.onload = () => { + resolve(fr.result); + }; + fr.readAsArrayBuffer(blob); + }); + }).then(sound => { + newSong.sound = sound; + this.filesLoaded++; + this.updateProgress(); + }); + } + } -Respack.prototype.parseFile = function(file) { - let name = file.name; - if (name.match(this.audioExtensions)) { - this.songQueue.push(this.parseSong(file)); - this.filesToLoad++; - } else if (name.match(this.imageExtensions)) { - this.imageQueue.push(this.parseImage(file)); - this.filesToLoad++; + parseSongQueue() { + return this.songQueue.reduce((sequence, songPromise) => { + return sequence.then(() => { + // Maintain order + return songPromise; + }); + }, Promise.resolve()); } - else if(name.toLowerCase().endsWith(".xml")){ - this._xmlQueue.push(this.loadXML(file)); + + parseImage(file) { + let match; + let name = file.name.replace(imageExtensions, ""); + let img; + + // Animation + if((match = name.match(new RegExp("^(.*)_(\\d+)$")))) { + img = this.getImage(match[1]); + if(!img) { // make a fresh one + img = {"name":match[1], + "fullname":match[1], + "align":"center", + //"crc":this.quickCRC(file), + "bitmaps":[], + "frameDurations":[33], + "source":null, + "enabled":true, + "animated":true, + "beatsPerAnim": null}; + this.images.push(img); + } + // Normal image + } else if (!this.containsImage(name)) { + img = {"name":name, + "fullname":name, + "bitmap":null, + "align":"center", + //"crc":this.quickCRC(file), + "source":null, + "enabled":true, + "filename":file.name, + "animated":false}; + this.images.push(img); + } else { + let existing = this.getImage(name); + console.log("WARNING: Image", name, "already exists! Conflict with", file.name, "and", existing.name); + return; + } + + return this.loadImage(file, img); } -}; -Respack.prototype.parseSong = function(file) { - let name = file.name.replace(this.audioExtensions, ""); - debug("parsing song: " + name); - if (this.containsSong(name)) { - let oldSong = this.getSong(name); - debug("WARNING: Song", name, "already exists! Conflict with", name, "and", oldSong.name); - } else { - let newSong = {"name":name, - "title":null, - "rhythm":null, - "source":null, - //"crc":this.quickCRC(file), TODO - "sound":null, - "enabled":true, - "filename":file.name, - "charsPerBeat": null}; - let extension = file.name.split('.').pop().toLowerCase(); + loadImage(imgFile, imageObj) { + let extension = imgFile.name.split('.').pop().toLowerCase(); let mime = ""; switch(extension) { - case "mp3": - mime = "audio/mpeg3"; + case "png": + mime = "image/png"; break; - case "ogg": - mime = "audio/ogg"; + case "gif": + mime = "image/gif"; break; - case "wav": - mime = "audio/wav"; + case "jpg": + case "jpeg": + mime = "image/jpeg"; break; default: mime = "application/octet-stream"; } - this.songs.push(newSong); return new Promise((resolve, reject) => { - file.getBlob(mime, sound => { - resolve(sound); - }); - }).then(blob => { - return new Promise((resolve, reject) => { - // Because blobs are crap - let fr = new FileReader(); - fr.onload = () => { - resolve(fr.result); - }; - fr.readAsArrayBuffer(blob); - }); - }).then(sound => { - newSong.sound = sound; + imgFile.getData64URI(mime, resolve); + }).then(bitmap => { this.filesLoaded++; this.updateProgress(); + return {bitmap: bitmap, img: imageObj}; }); } -}; - -Respack.prototype.parseSongQueue = function() { - return this.songQueue.reduce((sequence, songPromise) => { - return sequence.then(() => { - // Maintain order - return songPromise; - }); - }, Promise.resolve()); -}; -Respack.prototype.parseImage = function(file) { - let match; - let name = file.name.replace(this.imageExtensions, ""); - let img; - - // Animation - if((match = name.match(new RegExp("^(.*)_(\\d+)$")))) { - img = this.getImage(match[1]); - if(!img) { // make a fresh one - img = {"name":match[1], - "fullname":match[1], - "align":"center", - //"crc":this.quickCRC(file), - "bitmaps":[], - "frameDurations":[33], - "source":null, - "enabled":true, - "animated":true, - "beatsPerAnim": null}; - this.images.push(img); - } - // Normal image - } else if (!this.containsImage(name)) { - img = {"name":name, - "fullname":name, - "bitmap":null, - "align":"center", - //"crc":this.quickCRC(file), - "source":null, - "enabled":true, - "filename":file.name, - "animated":false}; - this.images.push(img); - } else { - let existing = this.getImage(name); - console.log("WARNING: Image", name, "already exists! Conflict with", file.name, "and", existing.name); - return; - } - - return this.loadImage(file, img); -}; - -Respack.prototype.loadImage = function(imgFile, imageObj) { - let extension = imgFile.name.split('.').pop().toLowerCase(); - let mime = ""; - switch(extension) { - case "png": - mime = "image/png"; - break; - case "gif": - mime = "image/gif"; - break; - case "jpg": - case "jpeg": - mime = "image/jpeg"; - break; - default: - mime = "application/octet-stream"; + parseImageQueue() { + return this.imageQueue.reduce((sequence, imagePromise) => { + return sequence.then(() => { + // Maintain order + return imagePromise; + }).then(response => { + // Don't crash if the respack had duplicate images + if(!response) + return; + let newImg = new Image(); + newImg.src = response.bitmap; + if (response.img.animated) { + response.img.bitmaps.push(newImg); + } else { + response.img.bitmap = newImg; + } + }); + }, Promise.resolve()); } - return new Promise((resolve, reject) => { - imgFile.getData64URI(mime, resolve); - }).then(bitmap => { - this.filesLoaded++; - this.updateProgress(); - return {bitmap: bitmap, img: imageObj}; - }); -}; - -Respack.prototype.parseImageQueue = function() { - return this.imageQueue.reduce((sequence, imagePromise) => { - return sequence.then(() => { - // Maintain order - return imagePromise; - }).then(response => { - // Don't crash if the respack had duplicate images - if(!response) - return; - let newImg = new Image(); - newImg.src = response.bitmap; - if (response.img.animated) { - response.img.bitmaps.push(newImg); - } else { - response.img.bitmap = newImg; - } - }); - }, Promise.resolve()); -}; -Respack.prototype.loadXML = function(file) { - return new Promise((resolve, reject) => { - file.getText(text => { - //XML parser will complain about a bare '&', but some respacks use & - text = text.replace(/&/g, '&'); - text = text.replace(/&/g, '&'); - let parser = new DOMParser(); - let dom = parser.parseFromString(text, "text/xml"); - resolve(dom); - }); - }); -}; - -Respack.prototype.parseXML = function() { - for(let i = 0; i < this._xmlQueue.length; i++) { - this._xmlQueue[i] = this._xmlQueue[i].then(dom => { - switch(dom.documentElement.nodeName) { - case "songs": - if(this.songs.length > 0) - this.parseSongFile(dom); - break; - case "images": - if(this.images.length > 0) - this.parseImageFile(dom); - break; - case "info": - this.parseInfoFile(dom); - break; - default: - console.log("XML found with no songs, images or info"); - break; - } + loadXML(file) { + return new Promise((resolve, reject) => { + file.getText(text => { + //XML parser will complain about a bare '&', but some respacks use & + text = text.replace(/&/g, '&'); + text = text.replace(/&/g, '&'); + let parser = new DOMParser(); + let dom = parser.parseFromString(text, "text/xml"); + resolve(dom); + }); }); } - return Promise.all(this._xmlQueue); -}; -// Save some chars -Element.prototype.getTag = function(tag, def) { - let t = this.getElementsByTagName(tag)[0]; - return t ? t.textContent : (def ? def : null); -}; + parseXML() { + for(let i = 0; i < this._xmlQueue.length; i++) { + this._xmlQueue[i] = this._xmlQueue[i].then(dom => { + switch(dom.documentElement.nodeName) { + case "songs": + if(this.songs.length > 0) + this.parseSongFile(dom); + break; + case "images": + if(this.images.length > 0) + this.parseImageFile(dom); + break; + case "info": + this.parseInfoFile(dom); + break; + default: + console.log("XML found with no songs, images or info"); + break; + } + }); + } + return Promise.all(this._xmlQueue); + } -Respack.prototype.parseSongFile = function(dom) { - debug(" - Parsing songFile"); - - let newSongs = []; - let el = dom.documentElement.firstElementChild; - for(; el; el = el.nextElementSibling) { - let song = this.getSong(el.attributes[0].value); - if(song) { - song.title = el.getTag("title"); - if(!song.title) { - song.title = ""; - debug(" WARNING!", song.name, "has no title!"); - } + parseSongFile(dom) { + debug(" - Parsing songFile"); + + let newSongs = []; + let el = dom.documentElement.firstElementChild; + for(; el; el = el.nextElementSibling) { + let song = this.getSong(el.attributes[0].value); + if(song) { + song.title = el.getTag("title"); + if(!song.title) { + song.title = ""; + debug(" WARNING!", song.name, "has no title!"); + } - song.rhythm = el.getTag("rhythm"); - if(!song.rhythm) { - song.rhythm = "..no..rhythm.."; - debug(" WARNING!!", song.name, "has no rhythm!!"); - } + song.rhythm = el.getTag("rhythm"); + if(!song.rhythm) { + song.rhythm = "..no..rhythm.."; + debug(" WARNING!!", song.name, "has no rhythm!!"); + } - song.buildupName = el.getTag("buildup"); - if(song.buildupName) { - debug(" Finding a buildup '" + song.buildupName + "' for ", song.name); - let 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.buildupName + "'!"); + song.buildupName = el.getTag("buildup"); + if(song.buildupName) { + debug(" Finding a buildup '" + song.buildupName + "' for ", song.name); + let 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.buildupName + "'!"); + } } - } - song.buildupRhythm = el.getTag("buildupRhythm"); - song.independentBuild = el.getTag("independentBuild"); - song.source = el.getTag("source"); - song.charsPerBeat = parseFloat(el.getTag("charsPerBeat")); + song.buildupRhythm = el.getTag("buildupRhythm"); + song.independentBuild = el.getTag("independentBuild"); + song.source = el.getTag("source"); + song.charsPerBeat = parseFloat(el.getTag("charsPerBeat")); - // Because PackShit breaks everything - if(this.name == "PackShit") { - song.forceTrim = true; + // Because PackShit breaks everything + if(this.name == "PackShit") { + song.forceTrim = true; + } + newSongs.push(song); + debug(" [I] " + song.name, ": '" + song.title + "' added to songs"); + } else { + debug(" WARNING!", "songs.xml: element", i + 1, + "- no song '" + el.attributes[0].value + "' found"); } - newSongs.push(song); - debug(" [I] " + song.name, ": '" + song.title + "' added to songs"); - } else { - debug(" WARNING!", "songs.xml: element", i + 1, - "- no song '" + el.attributes[0].value + "' found"); } - } - for(let i = 0; i < this.songs.length; i++) { - if(newSongs.indexOf(this.songs[i]) == -1) { - debug(" WARNING!", "We have a file for", this.songs[i].name, "but no information for it"); + for(let i = 0; i < this.songs.length; i++) { + if(newSongs.indexOf(this.songs[i]) == -1) { + debug(" WARNING!", "We have a file for", this.songs[i].name, "but no information for it"); + } } + this.songs = newSongs; } - this.songs = newSongs; -}; -Respack.prototype.parseInfoFile = function(dom) { - debug(" - Parsing infoFile"); + parseInfoFile(dom) { + debug(" - Parsing infoFile"); - let info = dom.documentElement; + let info = dom.documentElement; - // self reference strings to avoid changing strings twice in future - this.name = info.getTag("name", this.name); - this.author = info.getTag("author", this.author); - this.description = info.getTag("description", this.description); - this.link = info.getTag("link", this.link); -}; + // self reference strings to avoid changing strings twice in future + this.name = info.getTag("name", this.name); + this.author = info.getTag("author", this.author); + this.description = info.getTag("description", this.description); + this.link = info.getTag("link", this.link); + } -Respack.prototype.parseImageFile = function(dom) { - debug(" - Parsing imagefile"); - - let newImages = []; - let el = dom.documentElement.firstElementChild; - for(; el; el = el.nextElementSibling) { - let image = this.getImage(el.attributes[0].value); - if(image) { - image.fullname = el.getTag("fullname"); - if(!image.fullname) { - debug(" WARNING!", image.name, "has no full name!"); - } - image.source = el.getTag("source"); - // self reference defaults to avoid changing strings twice in future - image.align = el.getTag("align", image.align); - image.beatsPerAnim = parseFloat(el.getTag("beatsPerAnim")); - image.syncOffset = parseFloat(el.getTag("syncOffset")); - let frameDur = el.getTag("frameDuration"); - if(frameDur) { - image.frameDurations = []; - let strSplit = frameDur.split(","); - for(let j = 0; j < strSplit.length; j++) { - image.frameDurations.push(parseInt(strSplit[j])); + parseImageFile(dom) { + debug(" - Parsing imagefile"); + + let newImages = []; + let el = dom.documentElement.firstElementChild; + for(; el; el = el.nextElementSibling) { + let image = this.getImage(el.attributes[0].value); + if(image) { + image.fullname = el.getTag("fullname"); + if(!image.fullname) { + debug(" WARNING!", image.name, "has no full name!"); + } + image.source = el.getTag("source"); + // self reference defaults to avoid changing strings twice in future + image.align = el.getTag("align", image.align); + image.beatsPerAnim = parseFloat(el.getTag("beatsPerAnim")); + image.syncOffset = parseFloat(el.getTag("syncOffset")); + let frameDur = el.getTag("frameDuration"); + if(frameDur) { + image.frameDurations = []; + let strSplit = frameDur.split(","); + for(let j = 0; j < strSplit.length; j++) { + image.frameDurations.push(parseInt(strSplit[j])); + } + while (image.frameDurations.length < image.bitmaps.length) { + image.frameDurations.push(image.frameDurations[image.frameDurations.length - 1]); + } + debug("Frame durations:", image.frameDurations); + } + debug(" [I] " + image.name, ":", image.fullname, "added to images"); + if (image.bitmap || image.bitmaps) { + newImages.push(image); } - while (image.frameDurations.length < image.bitmaps.length) { - image.frameDurations.push(image.frameDurations[image.frameDurations.length - 1]); + else { + debug(" WARNING!!", "Image", image.name, "has no bitmap nor animation frames!"); } - debug("Frame durations:", image.frameDurations); + } else { + debug(" WARNING!", "images.xml: no image '" + el.attributes[0].value + "' found"); } - debug(" [I] " + image.name, ":", image.fullname, "added to images"); - if (image.bitmap || image.bitmaps) { + } + for(let i = 0; i < this.images.length; i++) { + let image = this.images[i]; + // Add all images with no info + if(newImages.indexOf(image) == -1) { newImages.push(image); } - else { - debug(" WARNING!!", "Image", image.name, "has no bitmap nor animation frames!"); - } - } else { - debug(" WARNING!", "images.xml: no image '" + el.attributes[0].value + "' found"); - } - } - for(let i = 0; i < this.images.length; i++) { - let image = this.images[i]; - // Add all images with no info - if(newImages.indexOf(image) == -1) { - newImages.push(image); } + newImages.sort(function(a, b) { + return a.name.localeCompare(b.name); + }); + this.images = newImages; } - newImages.sort(function(a, b) { - return a.name.localeCompare(b.name); - }); - this.images = newImages; -}; -Respack.prototype.containsSong = function(name) { - return this.getSong(name) !== null; -}; + containsSong(name) { + return this.getSong(name) !== null; + } -Respack.prototype.containsImage = function(name) { - return this.getImage(name) !== null; -}; + containsImage(name) { + return this.getImage(name) !== null; + } -Respack.prototype.getSong = function(name) { - for(let i = 0; i < this.songs.length; i++) { - if (name == this.songs[i].name) { - return this.songs[i]; + getSong(name) { + for(let i = 0; i < this.songs.length; i++) { + if (name == this.songs[i].name) { + return this.songs[i]; + } } + return null; } - return null; -}; -Respack.prototype.getImage = function(name) { - for(let i = 0; i < this.images.length; i++) { - if (name == this.images[i].name) { - return this.images[i]; + getImage(name) { + for(let i = 0; i < this.images.length; i++) { + if (name == this.images[i].name) { + return this.images[i]; + } } + return null; } - return null; -}; +} window.Respack = Respack; +// Save some chars +Element.prototype.getTag = function(tag, def) { + let t = this.getElementsByTagName(tag)[0]; + return t ? t.textContent : (def ? def : null); +}; + })(window, document); \ No newline at end of file diff --git a/src/js/SoundManager.js b/src/js/SoundManager.js index fade1a2..667cc34 100644 --- a/src/js/SoundManager.js +++ b/src/js/SoundManager.js @@ -22,531 +22,533 @@ (function(window, document) { "use strict"; -function SoundManager(core) { - this.core = core; - this.playing = false; - this.playbackRate = 1; - this.song = null; - - this.initPromise = null; - this.lockedPromise = null; - this.locked = true; - - /* Lower level audio and timing info */ - this.context = null; // Audio context, Web Audio API - this.oggSupport = false; - this.buildSource = null; - this.loopSource = null; - this.buildup = null; - this.loop = null; - this.startTime = 0; // File start time - 0 is loop start, not build start - this.buildLength = 0; - this.loopLength = 0; // For calculating beat lengths - - // Volume - this.gainNode = null; - this.mute = false; - this.lastVol = 1; - - // Visualiser - this.vReady = false; - this.vBars = 0; - this.vTotalBars = 0; - this.splitter = null; - this.analysers = []; - this.analyserArrays = []; - this.logArrays = []; - this.binCutoffs = []; - this.linBins = 0; - this.logBins = 0; - this.maxBinLin = 0; -} +class SoundManager { + constructor(core) { + this.core = core; + this.playing = false; + this.playbackRate = 1; + this.song = null; + + this.initPromise = null; + this.lockedPromise = null; + this.locked = true; + + /* Lower level audio and timing info */ + this.context = null; // Audio context, Web Audio API + this.oggSupport = false; + this.buildSource = null; + this.loopSource = null; + this.buildup = null; + this.loop = null; + this.startTime = 0; // File start time - 0 is loop start, not build start + this.buildLength = 0; + this.loopLength = 0; // For calculating beat lengths -SoundManager.prototype.init = function() { - if(!this.initPromise) { - this.initPromise = new Promise((resolve, reject) => { - // Check Web Audio API Support - try { - // More info at http://caniuse.com/#feat=audio-api - window.AudioContext = window.AudioContext || window.webkitAudioContext; - // These don't always exist - AudioContext.prototype.suspend = AudioContext.prototype.suspend || (() => {return Promise.resolve();}); - AudioContext.prototype.resume = AudioContext.prototype.resume || (() => {return Promise.resolve();}); - - this.context = new window.AudioContext(); - this.gainNode = this.context.createGain(); - this.gainNode.connect(this.context.destination); - } catch(e) { - reject(Error("Web Audio API not supported in this browser.")); - return; - } - resolve(); - }).then(() => { - // check for .ogg support - if not, we'll have to load the ogg decoder - return new Promise((resolve, reject) => { - this.context.decodeAudioData(miniOgg, success => { - this.oggSupport = true; - resolve(); - }, error => { - this.oggSupport = false; - resolve(); - }); - }); - }).then(() => { - return new Promise((resolve, reject) => { - // See if our audio decoder is working - let audioWorker; + // Volume + this.gainNode = null; + this.mute = false; + this.lastVol = 1; + + // Visualiser + this.vReady = false; + this.vBars = 0; + this.vTotalBars = 0; + this.splitter = null; + this.analysers = []; + this.analyserArrays = []; + this.logArrays = []; + this.binCutoffs = []; + this.linBins = 0; + this.logBins = 0; + this.maxBinLin = 0; + } + + init() { + if(!this.initPromise) { + this.initPromise = new Promise((resolve, reject) => { + // Check Web Audio API Support try { - audioWorker = this.createWorker(); + // More info at http://caniuse.com/#feat=audio-api + window.AudioContext = window.AudioContext || window.webkitAudioContext; + // These don't always exist + AudioContext.prototype.suspend = AudioContext.prototype.suspend || (() => {return Promise.resolve();}); + AudioContext.prototype.resume = AudioContext.prototype.resume || (() => {return Promise.resolve();}); + + this.context = new window.AudioContext(); + this.gainNode = this.context.createGain(); + this.gainNode.connect(this.context.destination); } catch(e) { - console.log(e); - reject(Error("Audio Worker cannot be started - correct path set in defaults?")); + reject(Error("Web Audio API not supported in this browser.")); return; } - let pingListener = event => { - audioWorker.terminate(); - resolve(); - }; - audioWorker.addEventListener('message', pingListener, false); - audioWorker.addEventListener('error', () => { - reject(Error("Audio Worker cannot be started - correct path set in defaults?")); - }, false); - audioWorker.postMessage({ping:true, ogg:this.oggSupport}); + resolve(); + }).then(() => { + // check for .ogg support - if not, we'll have to load the ogg decoder + return new Promise((resolve, reject) => { + this.context.decodeAudioData(miniOgg, success => { + this.oggSupport = true; + resolve(); + }, error => { + this.oggSupport = false; + resolve(); + }); + }); + }).then(() => { + return new Promise((resolve, reject) => { + // See if our audio decoder is working + let audioWorker; + try { + audioWorker = this.createWorker(); + } catch(e) { + console.log(e); + reject(Error("Audio Worker cannot be started - correct path set in defaults?")); + return; + } + let pingListener = event => { + audioWorker.terminate(); + resolve(); + }; + audioWorker.addEventListener('message', pingListener, false); + audioWorker.addEventListener('error', () => { + reject(Error("Audio Worker cannot be started - correct path set in defaults?")); + }, false); + audioWorker.postMessage({ping:true, ogg:this.oggSupport}); + }); + }).then(() => { + this.locked = this.context.state != "running"; }); - }).then(() => { - this.locked = this.context.state != "running"; - }); + } + return this.initPromise; } - return this.initPromise; -}; -SoundManager.prototype.unlock = function() { - if(this.lockedPromise) { + unlock() { + if(this.lockedPromise) { + return this.lockedPromise; + } + this.lockedPromise = new Promise((resolve, reject) => { + // iOS and other some mobile browsers - unlock the context as + // it starts in a suspended state + let unlocker = () => { + // create empty buffer + let buffer = this.context.createBuffer(1, 1, 22050); + let source = this.context.createBufferSource(); + source.buffer = buffer; + + // connect to output (your speakers) + source.connect( this.context.destination); + + // play the file + source.start(0); + + window.removeEventListener('touchend', unlocker); + window.removeEventListener('click', unlocker); + this.core.clearMessage(); + resolve(); + }; + window.addEventListener('touchend', unlocker, false); + window.addEventListener('click', unlocker, false); + }); return this.lockedPromise; } - this.lockedPromise = new Promise((resolve, reject) => { - // iOS and other some mobile browsers - unlock the context as - // it starts in a suspended state - let unlocker = () => { - // create empty buffer - let buffer = this.context.createBuffer(1, 1, 22050); - let source = this.context.createBufferSource(); - source.buffer = buffer; - - // connect to output (your speakers) - source.connect( this.context.destination); - - // play the file - source.start(0); + + playSong(song, playBuild, forcePlay) { + let p = Promise.resolve(); + // Editor forces play on audio updates + if(this.song == song && !forcePlay) { + return p; + } + this.stop(); + this.song = song; + if(!song || (!song.sound)) { // null song + return p; + } + + // if there's a fadeout happening from AutoSong, kill it + this.gainNode.gain.cancelScheduledValues(0); + // Reset original volume + this.setVolume(this.lastVol); + if(this.mute) { + this.setMute(true); + } + + p = p.then(() => { + return this.loadSong(song); + }).then(buffers => { + // To prevent race condition if you press "next" twice fast + if(song != this.song) { + return Promise.reject("Song changed between load and play - this message can be ignored"); + } - window.removeEventListener('touchend', unlocker); - window.removeEventListener('click', unlocker); - this.core.clearMessage(); - resolve(); - }; - window.addEventListener('touchend', unlocker, false); - window.addEventListener('click', unlocker, false); - }); - return this.lockedPromise; -}; - -SoundManager.prototype.playSong = function(song, playBuild, forcePlay) { - let p = Promise.resolve(); - // Editor forces play on audio updates - if(this.song == song && !forcePlay) { + this.buildup = buffers.buildup; + this.buildLength = this.buildup ? this.buildup.duration : 0; + this.loop = buffers.loop; + this.loopLength = this.loop.duration; + + // This fixes sync issues on Firefox and slow machines. + return this.context.suspend(); + }).then(() => { + if(playBuild) { + this.seek(-this.buildLength, true); + } else { + this.seek(0, true); + } + + return this.context.resume(); + }).then(() => { + this.playing = true; + }); return p; } - this.stop(); - this.song = song; - if(!song || (!song.sound)) { // null song - return p; + + stop(dontDeleteBuffers) { + if (this.playing) { + if(this.buildSource) { + this.buildSource.stop(0); + this.buildSource.disconnect(); + this.buildSource = null; + if(!dontDeleteBuffers) + this.buildup = null; + } + // arg required for mobile webkit + this.loopSource.stop(0); + // TODO needed? + this.loopSource.disconnect(); + this.loopSource = null; + if(!dontDeleteBuffers) + this.loop = null; + this.vReady = false; + this.playing = false; + this.startTime = 0; + } } - - // if there's a fadeout happening from AutoSong, kill it - this.gainNode.gain.cancelScheduledValues(0); - // Reset original volume - this.setVolume(this.lastVol); - if(this.mute) { - this.setMute(true); + + setRate(rate) { + // Double speed is more than enough. Famous last words? + rate = Math.max(Math.min(rate, 2), 0.25); + + let time = this.clampedTime(); + this.playbackRate = rate; + this.seek(time); } - p = p.then(() => { - return this.loadSong(song); - }).then(buffers => { - // To prevent race condition if you press "next" twice fast - if(song != this.song) { - return Promise.reject("Song changed between load and play - this message can be ignored"); + seek(time, noPlayingUpdate) { + if(!this.song) { + return; + } + //console.log("Seeking to " + time); + // Clamp the blighter + time = Math.min(Math.max(time, -this.buildLength), this.loopLength); + + this.stop(true); + + if(!this.loop) { + return; } + + 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; + this.loopSource.connect(this.gainNode); - this.buildup = buffers.buildup; - this.buildLength = this.buildup ? this.buildup.duration : 0; - this.loop = buffers.loop; - this.loopLength = this.loop.duration; - - // This fixes sync issues on Firefox and slow machines. - return this.context.suspend(); - }).then(() => { - if(playBuild) { - this.seek(-this.buildLength, true); + if(time < 0 && this.buildup) { + 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.playbackRate)); } else { - this.seek(0, true); + this.loopSource.start(0, time); } - return this.context.resume(); - }).then(() => { - this.playing = true; - }); - return p; -}; - -SoundManager.prototype.stop = function(dontDeleteBuffers) { - if (this.playing) { - if(this.buildSource) { - this.buildSource.stop(0); - this.buildSource.disconnect(); - this.buildSource = null; - if(!dontDeleteBuffers) - this.buildup = null; - } - // arg required for mobile webkit - this.loopSource.stop(0); - // TODO needed? - this.loopSource.disconnect(); - this.loopSource = null; - if(!dontDeleteBuffers) - this.loop = null; - this.vReady = false; - this.playing = false; - this.startTime = 0; - } -}; - -SoundManager.prototype.setRate = function(rate) { - // Double speed is more than enough. Famous last words? - rate = Math.max(Math.min(rate, 2), 0.25); - - let time = this.clampedTime(); - this.playbackRate = rate; - this.seek(time); -}; - -SoundManager.prototype.seek = function(time, noPlayingUpdate) { - if(!this.song) { - return; + this.startTime = this.context.currentTime - (time / this.playbackRate); + if(!noPlayingUpdate) { + this.playing = true; + } + this.initVisualiser(); + this.core.recalcBeatIndex(); } - //console.log("Seeking to " + time); - // Clamp the blighter - time = Math.min(Math.max(time, -this.buildLength), this.loopLength); - - this.stop(true); - - if(!this.loop) { - return; + + // In seconds, relative to the loop start + currentTime() { + if(!this.playing) { + return 0; + } + return (this.context.currentTime - this.startTime) * this.playbackRate; } + + clampedTime() { + let time = this.currentTime(); - 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; - this.loopSource.connect(this.gainNode); - - if(time < 0 && this.buildup) { - 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.playbackRate)); - } else { - this.loopSource.start(0, time); - } - - this.startTime = this.context.currentTime - (time / this.playbackRate); - if(!noPlayingUpdate) { - this.playing = true; - } - this.initVisualiser(); - this.core.recalcBeatIndex(); -}; - -// In seconds, relative to the loop start -SoundManager.prototype.currentTime = function() { - if(!this.playing) { - return 0; - } - return (this.context.currentTime - this.startTime) * this.playbackRate; -}; - -SoundManager.prototype.clampedTime = function() { - let time = this.currentTime(); - - if(time > 0) { - time %= this.loopLength; - } - return time; -}; - -SoundManager.prototype.loadSong = function(song) { - if(song._loadPromise) { - /* Caused when moving back/forwards rapidly. - The sound is still loading. We reject this promise, and the already - running decode will finish and resolve instead. - NOTE: If anything but playSong calls loadSong, this idea is broken. */ - return Promise.reject("Song changed between load and play - this message can be ignored"); - } - - let buffers = {loop: null, buildup: null}; - - let promises = [this.loadBuffer(song, "sound").then(buffer => { - buffers.loop = buffer; - })]; - if(song.buildup) { - promises.push(this.loadBuffer(song, "buildup").then(buffer => { - buffers.buildup = buffer; - })); - } else { - this.buildLength = 0; + if(time > 0) { + time %= this.loopLength; + } + return time; } - song._loadPromise = Promise.all(promises) - .then(() => { - song._loadPromise = null; - return buffers; - }); - return song._loadPromise; -}; - -SoundManager.prototype.loadBuffer = function(song, soundName) { - let buffer = song[soundName]; - - // Is this an ogg file? - let view = new Uint8Array(buffer); - // Signature for ogg file: OggS - if(this.oggSupport && view[0] == 0x4F && view[1] == 0x67 && view[2] == 0x67 && view[3] == 0x53) { - // As we don't control decodeAudioData, we cannot do fast transfers and must copy - let backup = buffer.slice(0); - return new Promise((resolve, reject) => { - this.context.decodeAudioData(buffer, result => { - resolve(result); - }, error => { - reject(Error("decodeAudioData failed to load track")); - }); - }).then(result => { - // restore copied buffer - song[soundName] = backup; - return result; - }); - } else { // Use our JS decoder - return new Promise((resolve, reject) => { - let audioWorker = this.createWorker(); - - audioWorker.addEventListener('error', () => { - reject(Error("Audio Worker failed to convert track")); - }, false); - - audioWorker.addEventListener('message', e => { - let decoded = e.data; - audioWorker.terminate(); - - // restore transferred buffer - song[soundName] = decoded.arrayBuffer; - if(decoded.error) { - reject(new Error(decoded.error)); - return; - } - // Convert to real audio buffer - let audio = this.audioBufFromRaw(decoded.rawAudio); - resolve(audio); - }, false); - - // transfer the buffer to save time - audioWorker.postMessage({buffer: buffer, ogg: this.oggSupport}, [buffer]); - }); - } - -}; - -// Converts continuous PCM array to Web Audio API friendly format -SoundManager.prototype.audioBufFromRaw = function(raw) { - let buffer = raw.array; - let channels = raw.channels; - let samples = buffer.length/channels; - let audioBuf = this.context.createBuffer(channels, samples, raw.sampleRate); - for(let i = 0; i < channels; i++) { - // Offset is in bytes, length is in elements - let channel = new Float32Array(buffer.buffer , i * samples * 4, samples); - // Most browsers - if(typeof audioBuf.copyToChannel === "function") { - audioBuf.copyToChannel(channel, i, 0); - } else { // Safari, Edge sometimes - audioBuf.getChannelData(i).set(channel); + + loadSong(song) { + if(song._loadPromise) { + /* Caused when moving back/forwards rapidly. + The sound is still loading. We reject this promise, and the already + running decode will finish and resolve instead. + NOTE: If anything but playSong calls loadSong, this idea is broken. */ + return Promise.reject("Song changed between load and play - this message can be ignored"); } + + let buffers = {loop: null, buildup: null}; + + let promises = [this.loadBuffer(song, "sound").then(buffer => { + buffers.loop = buffer; + })]; + if(song.buildup) { + promises.push(this.loadBuffer(song, "buildup").then(buffer => { + buffers.buildup = buffer; + })); + } else { + this.buildLength = 0; + } + song._loadPromise = Promise.all(promises) + .then(() => { + song._loadPromise = null; + return buffers; + }); + return song._loadPromise; } - return audioBuf; -}; -SoundManager.prototype.createWorker = function() { - return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js'); -}; + loadBuffer(song, soundName) { + let buffer = song[soundName]; + + // Is this an ogg file? + let view = new Uint8Array(buffer); + // Signature for ogg file: OggS + if(this.oggSupport && view[0] == 0x4F && view[1] == 0x67 && view[2] == 0x67 && view[3] == 0x53) { + // As we don't control decodeAudioData, we cannot do fast transfers and must copy + let backup = buffer.slice(0); + return new Promise((resolve, reject) => { + this.context.decodeAudioData(buffer, result => { + resolve(result); + }, error => { + reject(Error("decodeAudioData failed to load track")); + }); + }).then(result => { + // restore copied buffer + song[soundName] = backup; + return result; + }); + } else { // Use our JS decoder + return new Promise((resolve, reject) => { + let audioWorker = this.createWorker(); + + audioWorker.addEventListener('error', () => { + reject(Error("Audio Worker failed to convert track")); + }, false); + + audioWorker.addEventListener('message', e => { + let decoded = e.data; + audioWorker.terminate(); + + // restore transferred buffer + song[soundName] = decoded.arrayBuffer; + if(decoded.error) { + reject(new Error(decoded.error)); + return; + } + // Convert to real audio buffer + let audio = this.audioBufFromRaw(decoded.rawAudio); + resolve(audio); + }, false); + + // transfer the buffer to save time + audioWorker.postMessage({buffer: buffer, ogg: this.oggSupport}, [buffer]); + }); + } -SoundManager.prototype.initVisualiser = function(bars) { - // When restarting the visualiser - if(!bars) { - bars = this.vTotalBars; } - this.vReady = false; - this.vTotalBars = bars; - for(let i = 0; i < this.analysers.length; i++) { - this.analysers[i].disconnect(); - } - if(this.splitter) { - this.splitter.disconnect(); - this.splitter = null; - } - this.analysers = []; - this.analyserArrays = []; - this.logArrays = []; - this.binCutoffs = []; - - this.linBins = 0; - this.logBins = 0; - this.maxBinLin = 0; - - this.attachVisualiser(); -}; - -SoundManager.prototype.attachVisualiser = function() { - if(!this.playing || this.vReady) { - return; + + // Converts continuous PCM array to Web Audio API friendly format + audioBufFromRaw(raw) { + let buffer = raw.array; + let channels = raw.channels; + let samples = buffer.length/channels; + let audioBuf = this.context.createBuffer(channels, samples, raw.sampleRate); + for(let i = 0; i < channels; i++) { + // Offset is in bytes, length is in elements + let channel = new Float32Array(buffer.buffer , i * samples * 4, samples); + // Most browsers + if(typeof audioBuf.copyToChannel === "function") { + audioBuf.copyToChannel(channel, i, 0); + } else { // Safari, Edge sometimes + audioBuf.getChannelData(i).set(channel); + } + } + return audioBuf; } - // Get our info from the loop - let channels = this.loopSource.channelCount; - // In case channel counts change, this is changed each time - this.splitter = this.context.createChannelSplitter(channels); - // Connect to the gainNode so we get buildup stuff too - this.loopSource.connect(this.splitter); - if(this.buildSource) { - this.buildSource.connect(this.splitter); + createWorker() { + return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js'); } - // Split display up into each channel - this.vBars = Math.floor(this.vTotalBars/channels); - - for(let i = 0; i < channels; i++) { - let analyser = this.context.createAnalyser(); - // big fft buffers are new-ish - try { - analyser.fftSize = 8192; - } catch(err) { - analyser.fftSize = 2048; + + initVisualiser(bars) { + // When restarting the visualiser + if(!bars) { + bars = this.vTotalBars; + } + this.vReady = false; + this.vTotalBars = bars; + for(let i = 0; i < this.analysers.length; i++) { + this.analysers[i].disconnect(); } - // Chosen because they look nice, no maths behind it - analyser.smoothingTimeConstant = 0.6; - analyser.minDecibels = -70; - analyser.maxDecibels = -25; - this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount)); - analyser.getByteTimeDomainData(this.analyserArrays[i]); - this.splitter.connect(analyser, i); - this.analysers.push(analyser); - this.logArrays.push(new Uint8Array(this.vBars)); + if(this.splitter) { + this.splitter.disconnect(); + this.splitter = null; + } + this.analysers = []; + this.analyserArrays = []; + this.logArrays = []; + this.binCutoffs = []; + + this.linBins = 0; + this.logBins = 0; + this.maxBinLin = 0; + + this.attachVisualiser(); } - let binCount = this.analysers[0].frequencyBinCount; - let binWidth = this.loopSource.buffer.sampleRate / binCount; - // first 2kHz are linear - this.maxBinLin = Math.floor(2000/binWidth); - // Don't stretch the first 2kHz, it looks awful - this.linBins = Math.min(this.maxBinLin, Math.floor(this.vBars/2)); - // Only go up to 22KHz - let maxBinLog = Math.floor(22000/binWidth); - let logBins = this.vBars - this.linBins; - - let logLow = Math.log2(2000); - let logDiff = Math.log2(22000) - logLow; - for(let i = 0; i < logBins; i++) { - let cutoff = i * (logDiff/logBins) + logLow; - let freqCutoff = Math.pow(2, cutoff); - let binCutoff = Math.floor(freqCutoff / binWidth); - this.binCutoffs.push(binCutoff); + + attachVisualiser() { + if(!this.playing || this.vReady) { + return; + } + + // Get our info from the loop + let channels = this.loopSource.channelCount; + // In case channel counts change, this is changed each time + this.splitter = this.context.createChannelSplitter(channels); + // Connect to the gainNode so we get buildup stuff too + this.loopSource.connect(this.splitter); + if(this.buildSource) { + this.buildSource.connect(this.splitter); + } + // Split display up into each channel + this.vBars = Math.floor(this.vTotalBars/channels); + + for(let i = 0; i < channels; i++) { + let analyser = this.context.createAnalyser(); + // big fft buffers are new-ish + try { + analyser.fftSize = 8192; + } catch(err) { + analyser.fftSize = 2048; + } + // Chosen because they look nice, no maths behind it + analyser.smoothingTimeConstant = 0.6; + analyser.minDecibels = -70; + analyser.maxDecibels = -25; + this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount)); + analyser.getByteTimeDomainData(this.analyserArrays[i]); + this.splitter.connect(analyser, i); + this.analysers.push(analyser); + this.logArrays.push(new Uint8Array(this.vBars)); + } + let binCount = this.analysers[0].frequencyBinCount; + let binWidth = this.loopSource.buffer.sampleRate / binCount; + // first 2kHz are linear + this.maxBinLin = Math.floor(2000/binWidth); + // Don't stretch the first 2kHz, it looks awful + this.linBins = Math.min(this.maxBinLin, Math.floor(this.vBars/2)); + // Only go up to 22KHz + let maxBinLog = Math.floor(22000/binWidth); + let logBins = this.vBars - this.linBins; + + let logLow = Math.log2(2000); + let logDiff = Math.log2(22000) - logLow; + for(let i = 0; i < logBins; i++) { + let cutoff = i * (logDiff/logBins) + logLow; + let freqCutoff = Math.pow(2, cutoff); + let binCutoff = Math.floor(freqCutoff / binWidth); + this.binCutoffs.push(binCutoff); + } + this.vReady = true; } - this.vReady = true; -}; -SoundManager.prototype.sumArray = function(array, low, high) { - let total = 0; - for(let i = low; i <= high; i++) { - total += array[i]; + sumArray(array, low, high) { + let total = 0; + for(let i = low; i <= high; i++) { + total += array[i]; + } + return total/(high-low+1); } - return total/(high-low+1); -}; -SoundManager.prototype.getVisualiserData = function() { - if(!this.vReady) { - return null; + getVisualiserData() { + if(!this.vReady) { + return null; + } + for(let a = 0; a < this.analyserArrays.length; a++) { + let data = this.analyserArrays[a]; + let result = this.logArrays[a]; + this.analysers[a].getByteFrequencyData(data); + + for(let i = 0; i < this.linBins; i++) { + let scaled = Math.round(i * this.maxBinLin / this.linBins); + result[i] = data[scaled]; + } + result[this.linBins] = data[this.binCutoffs[0]]; + for(let i = this.linBins+1; i < this.vBars; i++) { + let cutoff = i - this.linBins; + result[i] = this.sumArray(data, this.binCutoffs[cutoff-1], + this.binCutoffs[cutoff]); + } + } + return this.logArrays; } - for(let a = 0; a < this.analyserArrays.length; a++) { - let data = this.analyserArrays[a]; - let result = this.logArrays[a]; - this.analysers[a].getByteFrequencyData(data); - - for(let i = 0; i < this.linBins; i++) { - let scaled = Math.round(i * this.maxBinLin / this.linBins); - result[i] = data[scaled]; + + setMute(mute) { + if(!this.mute && mute) { // muting + this.lastVol = this.gainNode.gain.value; } - result[this.linBins] = data[this.binCutoffs[0]]; - for(let i = this.linBins+1; i < this.vBars; i++) { - let cutoff = i - this.linBins; - result[i] = this.sumArray(data, this.binCutoffs[cutoff-1], - this.binCutoffs[cutoff]); + if(mute) { + this.gainNode.gain.value = 0; + } else { + this.gainNode.gain.value = this.lastVol; } + this.core.userInterface.updateVolume(this.gainNode.gain.value); + this.mute = mute; + return mute; } - return this.logArrays; -}; -SoundManager.prototype.setMute = function(mute) { - if(!this.mute && mute) { // muting - this.lastVol = this.gainNode.gain.value; + toggleMute() { + return this.setMute(!this.mute); } - if(mute) { - this.gainNode.gain.value = 0; - } else { - this.gainNode.gain.value = this.lastVol; + + decreaseVolume() { + this.setMute(false); + let val = Math.max(this.gainNode.gain.value - 0.1, 0); + this.setVolume(val); + } + + increaseVolume() { + this.setMute(false); + let val = Math.min(this.gainNode.gain.value + 0.1, 1); + this.setVolume(val); + } + + setVolume(vol) { + this.gainNode.gain.value = vol; + this.lastVol = vol; + this.core.userInterface.updateVolume(vol); } - this.core.userInterface.updateVolume(this.gainNode.gain.value); - this.mute = mute; - return mute; -}; - -SoundManager.prototype.toggleMute = function() { - return this.setMute(!this.mute); -}; - -SoundManager.prototype.decreaseVolume = function() { - this.setMute(false); - let val = Math.max(this.gainNode.gain.value - 0.1, 0); - this.setVolume(val); -}; - -SoundManager.prototype.increaseVolume = function() { - this.setMute(false); - let val = Math.min(this.gainNode.gain.value + 0.1, 1); - this.setVolume(val); -}; - -SoundManager.prototype.setVolume = function(vol) { - this.gainNode.gain.value = vol; - this.lastVol = vol; - this.core.userInterface.updateVolume(vol); -}; - -SoundManager.prototype.fadeOut = function(callback) { - if(!this.mute) { - // Firefox hackery - this.gainNode.gain.setValueAtTime(this.lastVol, this.context.currentTime); - this.gainNode.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + 2); + + fadeOut(callback) { + if(!this.mute) { + // Firefox hackery + this.gainNode.gain.setValueAtTime(this.lastVol, this.context.currentTime); + this.gainNode.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + 2); + } + setTimeout(callback, 2000); } - setTimeout(callback, 2000); -}; +} let miniOggRaw = "T2dnUwACAAAAAAAAAADFYgAAAAAAAMLKRdwBHgF2b3JiaXMAAAAAAUSsAAAA" +