let sounds = {} let soundcloud = new AudioContext(); let recent = new Set() const mobile = ( /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) ) if (mobile) $('p[mobile]').each(function() { $(this).text($(this).attr('mobile')); $('.nomobile').hide() }) const intros = [ {name: $('#caption').text(), path: "assets/dont_you_lecture_me.wav" }, {name: "how you gonna talk behind my back when you deadass built like a", path: "assets/you_deadass_built_like_a.wav" }, {name: "white people be like", path: "assets/white_people_be_like.wav" } ] const actions = [ { shortcut: "t", action: "Set tempo", amount: true, name: "speed", image: "assets/action_speed.png", default: 300, set: [10, 10000], add: [-10000, 10000], multiply: [0.01, 1000, 0.1] }, { shortcut: "v", action: "Set volume", amount: true, name: "volume", image: "assets/action_volume.png", default: 100, set: [0, 600, 1, "%"], add: [-600, 600, 1, "%"], multiply: [0.01, 1000, 0.1] }, { shortcut: "p", action: "Pause for duration", amount: true, name: "stop", image: "assets/action_stop.png", default: 4, set: [0, 1000] }, { shortcut: "m", action: "Transpose", amount: true, name: "transpose", image: "assets/action_transpose.png", default: 1, set: [-60, 60], add: [-60, 60], multiply: [0.01, 100, 0.1] }, { shortcut: "l", action: "Loop", amount: true, name: "loopmany", image: "assets/action_loopmany.png", default: 4, set: [1, 1000] }, { shortcut: "r", action: "Loop once", name: "loop", image: "assets/action_loop.png" }, { shortcut: "s", action: "Set loop start point", name: "looptarget", image: "assets/action_looptarget.png" }, { shortcut: "c", action: "Combine sounds", name: "combine", image: "assets/action_combine.png" }, { shortcut: "g", action: "Go to target", isTarget: true, name: "jump", image: "assets/action_jump.png", set: [1, 9999] }, { shortcut: "a", action: "Target", isTarget: true, name: "target", image: "assets/action_target.png", set: [1, 9999] }, { shortcut: "x", action: "Stop all sounds", name: "cut", stopSounds: true, image: "assets/action_cut.png" }, { shortcut: "o", action: "Set start position", name: "startpos", image: "assets/action_startpos.png" }, { shortcut: "d", action: "Add divider", name: "divider", image: "assets/action_divider.png" }, { shortcut: "f", action: "Flash screen", name: "flash", image: "assets/action_flash.png" }, { shortcut: "u", action: "Pulse screen", amount: true, twoValues: [[0, 1000], [0.1, 128]], scroll: [0, 1], default: [1, 2], name: "pulse", image: "assets/action_pulse.png" }, { shortcut: "b", action: "Set background color", amount: true, colorMode: true, twoValues: [["color"], [0, 128]], scroll: [1, 0.25], default: ["X", 1], name: "bg", image: "assets/action_bg.png" } ] actions.forEach(x => { $('#actions').append(`
${x.action}${x.amount ? "

+

" : ""}
`) }) let soundList = [] fetch("./sounds.json").then(x => x.json()).then(list => { soundList = list $('#iconboxLoading').hide() $('#icons').removeClass('loadingIcons') list.forEach(x => { let imageLink = (!x.emoji && x.id.match(/[a-z0-9]/i)) ? `icons/${x.img || x.id}.png` : `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${(x.emoji || x.id).codePointAt(0).toString(16)}.svg` $('#icons').append(`
"tag_" + x).join(" ")}>${x.name}
`) lastGroup = x.group if (settings.proMode) { $('.hotbarTab').first().trigger('click') $('#proHotbar').show() $('#extraPadding').show() buildHotbar() } }) }).catch((e) => { console.error(e) $('#icons img').hide() $('#icons').addClass('loadingIcons') $('#iconboxLoading').show() $('#loadingText').text("Something went wrong!") $('#errorInfo').text(e.message) }) let hotbar = $('#hotbarNotes') function buildHotbar(filter="sounds") { $('#hotbarNotes .placed').removeClass('placed') hotbar.html("") // clear hotbar if (filter == "recent") { let recentNotes = [...recent].slice(-48).reverse() recentNotes.forEach(x => { if (x.startsWith(".")) hotbar.append($(`#actions div[action=${x.slice(1)}]`).prop('outerHTML') || "") // action else hotbar.append($(`#icons div[str=${x}]`).prop('outerHTML') || "") // sound }) return } // whether to pull from sounds, actions, or both let pool = (filter == "actions" ? "#actions div" : ["recent", "favorite"].includes(filter) ? "#icons div, #actions div" : "#icons div") $(pool).each(function() { let x = $(this) if (filter == "notes" && !x.is('[tag_note]')) return else if (filter == "percussion" && !x.is('[tag_percussion]')) return hotbar.append(x.prop('outerHTML')) }) } function updateRecent(id) { recent.delete(id); recent.add(id) } let defaultBG = "#36393c" let colorRegex = /^#[a-f0-9]{6}$/i let cloneSort = false $('#sequence').sortable({ tolerance: "pointer", helper: "clone", containment: "#sequence", placeholder: "sortPlaceholder", items: "div", cursor: "move", delay: mobile ? 1500 : 0, forcePlaceholderSize: true, forceHelperSize: true, start: function(event, ui) { cloneSort = shiftHeld || altHeld if (cloneSort) ui.item.addClass('shiftPlaceholder') ui.placeholder.html(ui.item.html()) }, stop: function(event, ui) { if (cloneSort) { ui.item.clone().insertAfter(ui.item); $(this).sortable('cancel'); ui.item.clone() } cloneSort = false $('.shiftPlaceholder').removeClass('shiftPlaceholder') $('.placed').removeClass('placed') cancel() if ($("#sequence section").length > 1) { if (ui.item.attr("action") == "divider") deselectSection() syncSections() } } }) $(document).on('mouseover', '#icons div', function () { let soundCredit = $(this).attr('soundName') let soundOrigin = $(this).attr('soundOrigin') $('#soundText').hide() $('#soundName').text(soundCredit) $('#soundOrigin').text(soundOrigin ? ` (${soundOrigin})` : "") $('#soundInfo').show() }) $(document).on('mouseleave', '#icons div', function () { $('#soundInfo').hide() $('#soundText').show() }) $(document).on('mouseover', '#actions div', function () { $('#actionText').hide() $('#actionInfo').show() $('#actionName').text($(this).attr('info')) if (!mobile && !settings.noActionShortcuts) $('#actionKey').text("(" + $(this).attr('key') + ")").show() else $('#actionKey').hide() }) $(document).on('mouseleave', '#actions div', function () { $('#actionInfo').hide() $('#actionText').show() }) $(document).on('mouseover', '#sequence div', function () { if (shiftHeld || altHeld) $(this).addClass('holdingShift') }) $(document).on('mouseleave', '#sequence div', function () { $('.holdingShift').removeClass('holdingShift') }) $(document).on('mouseover', '#sequence section', function () { if (ctrlHeld && hasDividers()) $(this).addClass('holdingCtrl') }) $(document).on('mouseleave', '#sequence section', function () { $('.holdingCtrl').removeClass('holdingCtrl') }) $(document).on('mouseleave', '#sequence .removedDivider', function (e) { if (!e.isTrigger) $(this).removeClass('removedDivider') }) $(document).on('mouseover', '.hotbarTab', function() { $('#hotbarText').css('color', 'var(--yellowfont)').text($(this).attr('desc')).show() $('#hotbarContext').css('color', 'var(--yellowfont)').text("(" + ($(this).index() + 1) + ")").show() }) $(document).on('mouseover', '#hotbarNotes div.sound', function() { let origin = $(this).attr("soundOrigin") $('#hotbarText').css('color', 'var(--greenfont)').text($(this).attr('soundName')).show() if (!mobile) $('#hotbarContext').css('color', 'var(--greenfont)').text(origin ? `(${origin})` : "").show() }) $(document).on('mouseover', '#hotbarNotes div.action', function() { $('#hotbarText').css('color', 'var(--bluefont)').text($(this).attr('info')).show() if (!mobile && !settings.noActionShortcuts) $('#hotbarContext').css('color', 'var(--bluefont)').text("(" + $(this).attr('key') + ")").show() }) $(document).on('mouseleave', '#hotbarNotes div, .hotbarTab', function (e) { $('#hotbarText, #hotbarContext').text("").hide() }) // prevents right click menu from showing $(document).on('click contextmenu', '.iconbox, #proHotbar', function () { return false }) $(document).on('click contextmenu', '#icons div, #hotbarNotes div.sound', function (event) { cancel() let sound = $(this).attr('sound') let soundID = $(this).attr('str') let added = $(this.outerHTML) let pitch = null if (event.type == "contextmenu") { if (!settings.noAnimations) $(this).runAnimation('placed') playSound(sound) return false } if (pitch) added.append(`

${pitch >= 0 ? "+" : ""}${pitch}

`) // ??? if (!sound.startsWith("_")) { if (!settings.noAnimations) added.runAnimation('placed') playSound(sound, { pitch: semitonesToPercent(pitch)} ) } added.removeAttr("soundorigin") added.removeAttr("soundname") updateRecent(soundID); addToSequence(added) }) $(document).on('click contextmenu', '#actions div, #hotbarNotes div.action', function (event) { cancel() let actionName = $(this).attr('action') let action = actions.find(x => x.name == actionName) if (!action) return let added = $(this.outerHTML) added.removeAttr("info").removeAttr("key") if (action.amount) { stash = added if (event.type == "contextmenu" && action.default) { if (action.twoValues) return addAdvancedAction(actionName, action.default) else return addAction(actionName, action.default) } else { if (shiftHeld) stash.attr("addToStart", true) let actionPopup = $(`#action_${actionName}`) actionPopup.css('display', 'flex') // action editing - fill values if (replaceAction && replaceAction.attr("action") == actionName) { if (action.twoValues) { $(actionPopup.find('input').first().val(replaceAction.attr("val1"))).trigger('input') $(actionPopup.find('input').last().val(replaceAction.attr("val2"))).trigger('input') } else $(actionPopup.find('input').first().val(replaceAction.attr("amount"))).trigger('input') } return } } else if (action.isTarget) { let nextFree = 1 while ($(`.action[action=jump][amount=${nextFree}]`).length && $(`.action[action=target][amount=${nextFree}]`).length) nextFree++ added.attr("amount", nextFree).append(`

${nextFree}

`) } else if (action.set) { if (action.showPlus) added.attr("num", "plus") added.attr("amount", action.default).append(`

${getPrefix(added.attr("num"), action.default)}${action.default}

`) } if (action.stopSounds) stopSounds() if (!settings.noAnimations) added.addClass('placed') if (action.name == "divider") { if (!shiftHeld && selectedDivider >= 0) selectedDivider++ } updateRecent("." + actionName) addToSequence(added) }) function getPrefix(num, amt) { return num == "plus" ? (amt >= 0 ? "+" : "") : num == "add" ? "+" : num == "multiply" ? "⨯" : "" } function addToSequence(element, noPrepend, copyGroup) { let startAttr = element.attr("addToStart") if (startAttr) element.removeAttr("addToStart") let prependMode = (shiftHeld && !noPrepend) || startAttr let container = $(`#sequence section[group="${selectedDivider}"]`) if (copyGroup >= 0 && selectedDivider != copyGroup) container = [] if (!container.length) container = prependMode ? $(`#sequence section`).first() : $(`#sequence section`).last() if (!container.length) { // jquery isn't exactly good at 'or', whatever $("#sequence").append(`
`) return addToSequence(element) } prependMode ? container.prepend(element) : appendToSection(container, element) if (element.attr("action") == "divider") syncSections() } // add to end, or second last if it ends with a divider function appendToSection(container, element) { let lastChild = container.children().last() let hasDivider = (lastChild.attr("action") == "divider") if (hasDivider) element.insertBefore(lastChild) else container.append(element) } function addAction(action, input, num="set", element=stash, dontAppend=false) { if (typeof input == "string" && input.startsWith("#")) input = $(input).val() if (!element || isNaN(input)) return let amount = Number(Number(input).toFixed(3)) // tofixed converts to string lmao let foundAction = actions.find(x => x.name == action) let actionData = foundAction[num] amount = clamp(amount, actionData[0], actionData[1]) let prefix = getPrefix(num, amount) let amountStr = prefix + String(amount) + (actionData[3] || "") //element.attr("min", actionData[0]).attr("max", actionData[1]) if (num != "set") element.attr("num", num) if (!isNaN(actionData[2])) element.attr("step", actionData[2]) if (actionData[3]) element.attr("suffix", actionData[3]) if (!settings.noAnimations) element.addClass('placed') element.attr("amount", amount).find('p').text(amountStr) if (replaceAction) editAction(element) else if (!dontAppend) addToSequence(element) else return element updateRecent("." + action) if (stash) stash = null $('.popup').hide() } // eh i'm just gonna make a new function for this function addAdvancedAction(action, inputs, element=stash, dontAppend=false) { let foundAction = actions.find(x => x.name == action) if (!foundAction || !Array.isArray(inputs)) return let cleanInputs = inputs.map((x, y) => { let bounds = foundAction.twoValues[y] if (bounds == "color") x = x.match(colorRegex) ? x : defaultBG else if (Array.isArray(bounds)) x = clamp(x, bounds[0], bounds[1]) return x }) element.attr("advanced", true).attr("val1", cleanInputs[0]).attr("val2", cleanInputs[1]) if (foundAction.colorMode) element.find('p').html(` ${cleanInputs[1]}`) else element.find('p').text(`${cleanInputs[0]}, ${cleanInputs[1]}`) if (replaceAction) editAction(element) else if (!dontAppend) addToSequence(element) else return element if (stash) stash = null $('.popup').hide() } function editAction(element) { replaceAction.replaceWith(element) replaceAction.runAnimation('placed') replaceAction = null } function syncSections() { let noteGroups = [""] let dividerIndex = 0 let collapsedSections = [] $('.placed').removeClass('placed') $('.selectedDivider').removeClass('selectedDivider') $('#sequence div').each(function() { let isDivider = $(this).attr("action") == "divider" if (isDivider) { let oldSection = Number($(this).attr("section")) if (oldSection >= 0 && $(this).parent().hasClass("sectionHidden")) collapsedSections.push(dividerIndex) $(this).attr("section", dividerIndex) } noteGroups[dividerIndex] += $(this).prop("outerHTML") if (isDivider) { dividerIndex++ noteGroups.push("") } }) $('#sequence').html(noteGroups.map((x, y) => `
${x}
`).join("")) if (dividerIndex > 0) $('#sectionSettings').show() else $('#sectionSettings').hide() } function hasDividers() { return $("#sequence section").length > 1 } function deselectSection() { selectedDivider = -1 if (ctrlHeld) $('.selectedDivider').addClass('removedDivider') displaySection() } function changeSection(change, scroll) { let totalDividers = $('#sequence section').length if (totalDividers <= 1) return selectedDivider = -1 if (selectedDivider == -1 && change < 0) selectedDivider = totalDividers - 1 else if (selectedDivider == -1 && change > 0) selectedDivider = 0 else selectedDivider = selectedDivider + change if (selectedDivider < 0) selectedDivider = totalDividers - 1 else if (selectedDivider >= totalDividers) selectedDivider = 0 displaySection(scroll) } function displaySection(scroll) { $(".selectedDivider").removeClass("selectedDivider") if (selectedDivider >= 0) { let foundDivider = $(`#sequence section[group="${selectedDivider}"]`) foundDivider.addClass("selectedDivider") $('.requiresSelected').removeClass('cantSelect') $('#selectedSection').text(selectedDivider + 1) if (foundDivider.hasClass("sectionHidden")) { $('#hideSection').hide(); $('#showSection').show() } else { $('#hideSection').show(); $('#showSection').hide() } if (!active && scroll) $('#everything').stop().animate({ scrollTop: foundDivider.prop("offsetTop") - 150 }, 100) } else { $('.requiresSelected').addClass('cantSelect') $('#selectedSection').text("None") $('#hideSection').show(); $('#showSection').hide() } } function toggleSectionVisibility(section=selectedDivider) { if (section < 0) return $(`#sequence section[group="${section}"]`).toggleClass('sectionHidden') displaySection() } $(document).on('click contextmenu', '#sequence section', function (e) { if (!ctrlHeld) return e.stopPropagation() if (!hasDividers()) return let dividerGroup = parseInt($(this).attr("group")) if (isNaN(dividerGroup)) return if (e.type == "contextmenu") { toggleSectionVisibility(dividerGroup) return false } if ($(this).hasClass("selectedDivider")) return deselectSection() else { selectedDivider = dividerGroup if (isNaN(selectedDivider)) return selectedDivider = -1 displaySection() } }) $(document).on('click', '#sequence div', function () { if (ctrlHeld) return cancel() if (shiftHeld || altHeld) { let copy = $(this).clone() if (!settings.noAnimations) copy.addClass('placed') copy.insertAfter($(this)) playSound($(this).attr("sound"), { pitch: getPitch($(this)), volume: getVolume($(this)), stopPrevious: true }) } else $(this).remove() if ($(this).attr("action") == "divider") { deselectSection() syncSections() } }) $(document).on('contextmenu', '#sequence div', function () { if (ctrlHeld || active) return false let snd = $(this).attr("sound") let acn = $(this).attr("action") if (shiftHeld || altHeld) { // clone and append to end let copy = $(this).clone() if (!settings.noAnimations) copy.addClass('placed') addToSequence(copy, true, Number($(this).parent().attr("group"))) playSound(snd, { pitch: getPitch($(this)), volume: getVolume($(this)), stopPrevious: true}) return false } else if ($(this).hasClass("action") && acn) { let foundAction = actions.find(x => x.name == acn) if (!foundAction || !foundAction.amount) return false replaceAction = $(this) let actionBtn = $(`#actions div[action="${acn}"]`) if (!actionBtn.length) return actionBtn.trigger("click") return false } else if (!active && snd) { if (!settings.noAnimations) $(this).runAnimation('placed') playSound(snd, { pitch: getPitch($(this)), volume: getVolume($(this)), stopPrevious: true}) return false } }) // hotbar tabs $(document).on('click', '.hotbarTab:not(.selectedTab)', function () { $('.selectedTab').removeClass('selectedTab') $(this).addClass('selectedTab') buildHotbar($(this).attr("tab")) }) $("#everything").scroll(function(){ if (settings.dontFadeProBar || mobile) return let normal = 30 let top = 200 / 1.5 let dist = Math.min(normal, ($(this).scrollTop() - 200) / 1.5) let percent = Math.max(0, ((dist + top) / (normal + top)) * 100) $("#proHotbar").css("bottom", dist + "px") .css("opacity", percent + "%") .css("visibility", percent < 2 ? "hidden" : "visible") }); // no idea what this is for but probably some sorting bug function whatthefuck(el, index) { $('#sequence').sortable('cancel') el.remove().insertAfter('#sequence div')[index] } let lastY = null let mobileCooldown = false $(document).on('wheel touchmove', '#sequence div', function(event) { let el = $(this) if (event.type == "touchmove") { if (mobileCooldown || $('.ui-sortable-helper').length) return let clientY = event.originalEvent.touches[0].clientY; let sensitivity = 50 if (clientY > (lastY + sensitivity)) event.arrowDelta = 21 else if (clientY < (lastY - sensitivity)) event.arrowDelta = -21 else return lastY = clientY; mobileCooldown = true setTimeout(() => { mobileCooldown = false; $('#sequence div').scrollTop(200) }, 25); $('#sequence div').scrollTop(200) } if (active || $(this).hasClass('ui-sortable-helper')) return let isVolume = (ctrlHeld && el.attr("sound")) let downward = (event.arrowDelta || event.originalEvent.deltaY) > 0 let foundAction = actions.find(x => x.name == el.attr("action")) let foundText = el.attr("amount") || el.attr("pitch") || el.find("p").text() let shift = Number(foundText) || 0 if (isVolume) { shift = Number(el.attr("vol")) if (isNaN(shift) || shift < 0) shift = 100 } if (el.attr("sound") && !el.attr("str").startsWith("_")) { let shiftChange = ((downward ? -1 : 1) * (shiftHeld ? (isVolume ? 10 : 6) : altHeld ? 0.2 : 1)) if (!isVolume) { shift += shiftChange shift = Number(clamp(shift, -60, 60).toFixed(2)) let prefix = shift > 0 ? "+" : "" if (foundText && shift == 0) el.find("p").remove() else if (!el.find("p").length) el.append(`

${prefix + shift}

`) else el.find("p").text(prefix + shift) el.attr("pitch", shift) playSound(el.attr("sound"), { pitch: semitonesToPercent(shift), volume: getVolume(el), stopPrevious: true }) } else { if (shiftChange == 1 || shiftChange == -1) shiftChange *= 2 shift += shiftChange shift = Number(clamp(shift, 0, 400).toFixed(2)) if (shift == 100) el.find("vol").remove() else if (!el.find("vol").length) el.append(`${shift}%`) else el.find("vol").text(shift + "%") el.attr("vol", shift) playSound(el.attr("sound"), { pitch: getPitch(el), volume: shift / 100 / 2, stopPrevious: true }) } } else if (el.attr("action") && el.attr("amount")) { let bounds = foundAction[el.attr("num") || "set"] let step = bounds[2] || 1 if (shiftHeld) step *= 10 else if (altHeld) step /= 10 shift += downward ? step * -1 : step shift = Number(shift.toFixed(4)) shift = clamp(shift, bounds[0], bounds[1] || 999) el.attr("amount", shift) el.find("p").text(getPrefix(el.attr("num"), shift) + shift + (bounds[3] || "")) } else if (foundAction && el.attr("advanced")) { let scrollInfo = foundAction.scroll let valStr = scrollInfo[0] == 0 ? "val1" : "val2" let scrollVal = +el.attr(valStr) let twoBounds = foundAction.twoValues[scrollInfo[0]] let step = +scrollInfo[1] || 1 if (shiftHeld) step *= 10 else if (altHeld) step /= 10 scrollVal += downward ? step * -1 : step scrollVal = clamp(scrollVal.toFixed(4), twoBounds[0], twoBounds[1]) el.attr(valStr, scrollVal) if (foundAction.colorMode) el.find("p").children().last().text(el.attr("val2")) else el.find("p").text(`${el.attr("val1")}, ${el.attr("val2")}`) } }) // prevent ctrl+zoom $('.iconbox, #proHotbar').bind('mousewheel DOMMouseScroll', function(e) { if (e.ctrlKey) e.preventDefault(); }); let intro = 0 let lecture = null let cachedIntros = [] let mainNode = new GainNode(soundcloud) $('#caption').click(function() { intro++ if (intro >= intros.length) intro = 0 $('#caption').text(intros[intro].name).runAnimation('placed') stopIntro() loadIntro() cancel() }) function loadIntro() { let introPath = intros[intro].path if (cachedIntros[introPath]) lecture = cachedIntros[introPath] else { fetch(introPath) .then(res => res.arrayBuffer()) .then(buffer => soundcloud.decodeAudioData(buffer)) .then(data => { cachedIntros[introPath] = data lecture = data }) } } loadIntro() let activeIntro = null function playIntro() { stopIntro() let introSound = new AudioBufferSourceNode(soundcloud, { buffer: lecture }) mainNode.connect(soundcloud.destination) introSound.connect(mainNode) introSound.start() activeIntro = introSound introSound.addEventListener("ended", () => { if (introSound.dead) return beginSequence() stopIntro() }) } function stopIntro() { if (!activeIntro) return activeIntro.dead = true activeIntro.disconnect() activeIntro = null } document.addEventListener('scroll', function (event) { if ($(event.target).is("#sequence div")) $(event.target).scrollTop(200) }, true); $('#sequence').on('DOMSubtreeModified', function(event) { $('#sequence div').scrollTop(200) }); // https://stackoverflow.com/a/45036752 $.fn.runAnimation = function(className) { if (settings.noAnimations) return let el = $(this)[0] el.style.animation = "none" el.offsetHeight el.style.animation = null $(this).addClass(className) } let stash = null let replaceAction = null let defaultTempo = 300 let defaultVolume = 100 let preloaded = null let active = false let scrubbing = false let ctrlHeld = false let shiftHeld = false let altHeld = false let onCooldown = false let selectedDivider = -1 let volume = 0 updateTempo(defaultTempo) setVolume(defaultVolume) function clamp(num, min, max) { return Math.min(Math.max(num , min), max) } function percentToSemitones(percent) { return Math.log(percent, 2) * 12 } function semitonesToPercent(semitones) { return Math.pow(2, Number(semitones) / 12) } function getPitch(element) { return semitonesToPercent(element.find('p').text() || "0") } function getVolume(element) { return Number(element.find('vol').text().slice(0, -1) || 100) / 200 } function cancel(cancelOptions={}) { if ((!cancelOptions.keepAnimations && active) || cancelOptions.stopSounds) stopSounds() active = false preloaded = null updateTempo(defaultTempo) setVolume(100) if (!cancelOptions.keepAnimations) { $('.bounce').removeClass('bounce') $('.pulse').removeClass('pulse') } $('.playInfo').show() $('.stopInfo').hide() $('.pinnedHidden').removeClass("pinnedHidden") $('#proHotbar').removeClass("playing") $('.triggered').removeClass('triggered') $('#sequence p[triggeredCountdown]').each(function() { resetAmount($(this)) }) $('html').removeAttr("style") stopIntro() } function resetAmount(el) { let parent = el.parent() el.text(getPrefix(parent.attr('num'), +parent.attr('amount')) + parent.attr('amount') + (parent.attr('suffix') || "")) el.removeAttr("triggeredcountdown") } // on play button function startSequence(instant=false) { if (onCooldown || active || $('#sequence div').length < 1) return onCooldown = true setTimeout(() => { onCooldown = false }, 250); stopSounds() cancel() active = true $('#sectionSettings.pinnedSettings').addClass("pinnedHidden") $('#proHotbar').addClass("playing") $('.playInfo').hide() $('.stopInfo').show() preloaded = preloadSequence() if (!instant && selectedDivider < 0) playIntro() else beginSequence() } // after "don't you lecture me" function beginSequence() { if (!preloaded) return $('.placed').removeClass('placed') playSequence(preloaded) } function updateTempo(BPM) { $('#BPM').text(+BPM.toFixed(4)) } function setVolume(percent) { volume = Number(clamp(percent / 200, 0, 3).toFixed(4)) $('#volume').text(+(volume * 200).toFixed(4)) } // currently playing sounds are stored here let activeSounds = []; // remove any finished sounds from the active list function clearPlayedSounds() { activeSounds = activeSounds.filter(x => !x.sound.finished) } // destroy sound function killSound(snd, vol, clearList=true) { snd.finished = true snd.disconnect() vol.disconnect() if (clearList) clearPlayedSounds() } // stop all sounds function stopSounds() { activeSounds.forEach(x => killSound(x.sound, x.gainNode, false)) nextSoundToQueue = playingSequence.length; clearPlayedSounds() } // fetches and prepares sound let currentlyFetching = {} // prevent earrape when trying to load the same sound multiple times async function fetchSound(name) { if (!sounds[name] && name != "sounds/pause.wav") { currentlyFetching[name] = true let newSound = await fetch(name).then(res => res.arrayBuffer()).then(buffer => soundcloud.decodeAudioData(buffer)) sounds[name] = newSound delete currentlyFetching[name] } } async function playSound(name, soundSettings={}) { if (!name || name == "sounds/_pause.wav") return if (currentlyFetching[name] && !soundSettings.playAt) return if (!sounds[name]) await fetchSound(name) let snd = new AudioBufferSourceNode(soundcloud, { buffer: sounds[name], playbackRate: soundSettings.pitch || 1 }) let vol = new GainNode(soundcloud, { gain: !isNaN(soundSettings.volume) ? soundSettings.volume : 0.5 }) vol.connect(soundcloud.destination) snd.connect(vol) if (soundSettings.stopPrevious) { // clear all sounds with the same name let foundSameSounds = activeSounds.filter(x => x.name == name) foundSameSounds.forEach(x => killSound(x.sound, x.gainNode, false)) clearPlayedSounds() } let start = soundSettings.playAt || 0; activeSounds.push({sound: snd, gainNode: vol, index: soundSettings.index || 0, name, pitch: soundSettings.pitch || 1, volume, cut: false, start}) snd.start(start) snd.addEventListener("ended", () => { killSound(snd, vol, !active) }) } function cutSounds(time) { activeSounds.forEach(x => { if (!x.cut && x.start <= time) { x.cut = true; x.sound.stop(time); } }) } $('.skippableAction').each(function() { $(this).html('(right click to skip this popup)') $(this).attr("mobile", "(hold to skip this popup)") }) $('#clearsounds').click(function() { cancel({ stopSounds: true }); $('#sequence').html(''); $('#saveName').val(''); $('#sectionSettings').hide() $('.popup').hide(); filename = "sequence" saveLocation = null }) $(document).on('keydown', function(e) { if (e.originalEvent.repeat || e.target.nodeName == "INPUT" || mobile) return let popupVisible = $('.popup').is(":visible") if (e.which == 191) { // slash key (toggle menus) if (shiftHeld && !$('#shortcutMenu').is(":visible")) $('#settingsMenu').toggle() else if (ctrlHeld && !$('#settingsMenu').is(":visible")) $('#shortcutMenu').toggle() } else if (!popupVisible) { if ([13, 32].includes(e.which)) { // enter, space if (!active) $('#playBtn').triggerHandler('contextmenu') else $('#stopBtn').click() e.preventDefault() } else if ([38, 40].includes(e.which)) { // up, down if (ctrlHeld) { let change = e.which == 40 ? 1 : -1 changeSection(change, true) } else { $('#sequence div:hover').first().trigger({type: "wheel", arrowDelta: e.which == 40 ? 69 : -69}) e.preventDefault() } } else if (settings.proMode && e.which >= 48 && e.which <= 57) { // number keys let num = e.which - 48 if (num <= 0) return $('.hotbarTab').eq(num - 1).trigger('click') } else if (e.which == 83 && e.ctrlKey) { // ctrl + s $((sexySaving && !e.shiftKey) ? '#saveBtn' : '#downloadBtn').trigger('click') e.preventDefault() } else if (e.which == 79 && e.ctrlKey) { // ctrl + o $("#loadBtn").trigger('click') e.preventDefault() } else if (e.which == 68 && e.ctrlKey) { // ctrl + d deselectSection() e.preventDefault() } else if (e.which == 80 && e.ctrlKey) { $('#toggleProMode').trigger('click') e.preventDefault() } else if (!ctrlHeld && !altHeld && !settings.noActionShortcuts) { // actions let foundAction = actions.find(x => x.shortcut == e.key.toLowerCase()) if (foundAction) $(`#actions .action[action="${foundAction.name}"]`).trigger("click") } } // popups else { if (e.which == 27) { // esc $('.popup:not(.importantPopup)').hide() } else if (e.which == 13) { // enter $('button[actionConfirm=true]:visible').first().click() } } }); function updateKeys(e) { if (e.repeat || e.ctrlKey === undefined) return if (e.key === 'Alt') { e.preventDefault() } shiftHeld = e.shiftKey; altHeld = e.altKey; ctrlHeld = e.ctrlKey; $('#sequence div:hover').trigger((shiftHeld || altHeld) ? 'mouseover' : 'mouseleave') $('#sequence section:not(.ctrlHeld):hover').trigger(ctrlHeld ? 'mouseover' : 'mouseleave') } $(document).on('keyup keydown click wheel touchmove', updateKeys) $(window).on('blur focus', updateKeys) $(document).on('click', '.popup:not(.importantPopup)', function(e) { if ($(e.target).is('.popup')) { $('.popup').hide(); stash = null replaceAction = null } }); $('.colorPreview').click(function() { $(`.colorSelector[colorgroup=${$(this).attr("colorgroup")}`).trigger('click') }) $('.colorSelector').on('input', function() { $(`.colorPreview[colorgroup=${$(this).attr("colorgroup")}`).css("background-color", $(this).val()) $(`.colorTextbox[colorgroup=${$(this).attr("colorgroup")}`).val($(this).val().slice(1)) }) $('.colorTextbox').on('input', function() { let cleanVal = $(this).val().toLowerCase().replace(/[^a-f0-9]/g, "").slice(0, 6) $(this).val(cleanVal) let colorVal = "#" + cleanVal if (!colorVal.match(colorRegex)) return else $('.colorSelector').val(colorVal).trigger('input') }) // settings let settings = {} try { settings = localStorage["🗿"] ? JSON.parse(localStorage["🗿"]) : {} $('.settingBox[setting]').each(function() { let setting = $(this).attr('setting') let foundSetting = settings[setting] if ($(this).attr('inverted')) foundSetting = !foundSetting if (foundSetting) $(this).prop('checked', foundSetting) }) } catch(e) { console.error(e) } if (settings.pinSectionButtons) $('#sectionSettings').addClass("pinnedSettings") if (!settings.dontFadeProBar && settings.proMode) $('#everything').trigger('scroll') $(document).on('change click', '.settingBox', function() { let settingName = $(this).attr('setting') let val = $(this).prop('checked') if ($(this).attr('inverted')) val = !val if (!val) delete settings[settingName] else settings[settingName] = val localStorage["🗿"] = JSON.stringify(settings) if (settingName == "oldSaving") { if (val) disableNewSaving() else enableNewSaving() } else if (settingName == "pinSectionButtons") { if (settings.pinSectionButtons) { if (active) $('#sectionSettings').addClass("pinnedHidden") $('#sectionSettings').addClass("pinnedSettings") } else $('#sectionSettings').removeClass("pinnedSettings") } else if (settingName == "proMode") { if (settings.proMode) { $('.hotbarTab').first().trigger('click') $('#proHotbar').show() $('#extraPadding').show() let pageHeight = $('#everything').prop("scrollHeight") if (!ctrlHeld) $('#everything').animate({ scrollTop: pageHeight + 1000 }) } else { $('#proHotbar').hide() $('#extraPadding').hide() } } }); $('.extraSetting').hover(function() { $('#settingInfo').text($(this).attr("title")) $('#settingInfo').show() $('#settingHelp').hide() }, function() { $('#settingInfo').hide() $('#settingHelp').show() }) $('.extraSetting').click(function(e) { if (mobile || e.isTrigger || e.target.nodeName == "INPUT") return $(this).find('input').trigger('click') })