waves/public/assets/g/thirtydollarwebsite/🗿.js
2025-04-09 17:11:14 -05:00

1012 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(`<div class="action" action="${x.name}" info="${x.action}" key="${x.shortcut.toUpperCase()}"><img action="${x.name}" alt="${x.action}" src="${x.image}">${x.amount ? "<p>+</p>" : ""}</div>`) })
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(`<div class="sound" soundName="${x.name}" soundOrigin="${x.source || ""}" sound="sounds/${x.id}.wav" str="${x.id}" ${(x.tags || []).map(x => "tag_" + x).join(" ")}><img alt="${x.name}" src="${imageLink}"></div>`)
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(`<p>${pitch >= 0 ? "+" : ""}${pitch}</p>`) // ???
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(`<p>${nextFree}</p>`)
}
else if (action.set) {
if (action.showPlus) added.attr("num", "plus")
added.attr("amount", action.default).append(`<p>${getPrefix(added.attr("num"), action.default)}${action.default}</p>`)
}
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(`<section group="0"></section>`)
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(`<span style="color: ${cleanInputs[0]}; margin-right: 2.5px">⬤</span> <span>${cleanInputs[1]}<span>`)
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) => `<section class="${y == selectedDivider ? 'selectedDivider' : ''} ${collapsedSections.includes(y) ? "sectionHidden" : ""}" group="${y}">${x}</section>`).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(`<p>${prefix + shift}</p>`)
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(`<vol>${shift}%</vol>`)
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')
})