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.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(" ")}>

`)
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) => ``).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')
})