(function () {
let isPlaying = false;
let currentTracks = [];
let cursor = 0;
let howlerInstance;
const retroWaveRu = "https://retrowave.ru";
let titleEl = document.getElementById("track-name");
let coverArtEl = document.getElementsByClassName("music-player")[0];
let refreshBtn = document.querySelector(".refresh");
let ovr = document.querySelector(".OVR");
let fullScreenBtn = document.querySelector(".fullscreen");
let terminalBtn = document.querySelector(".terminal-btn");
let terminalOverlay = document.getElementById("terminal-overlay");
let terminalInput = document.getElementById("terminal-input");
let terminalOutput = document.getElementById("terminal-output");
let terminalClose = document.querySelector(".terminal-close");
let errorEl = document.querySelector(".ERRORS");
let currentEffectIndex = 0;
const modalEl = document.getElementById("intro-modal");
// const uploadInfoEl = document.getElementById("upload-info");
const fileUploadEl = document.getElementById("file-upload");
let volume = 1;
let effectCanvas;
let starField;
let line3D;
const synthwaveColor = 0xff2975;
openDialog();
listenUploadFileChange();
function initDynamicTooltips() {
document.querySelectorAll("[title]").forEach((element) => {
const title = element.getAttribute("title");
element.removeAttribute("title");
element.setAttribute("data-title", title);
element.addEventListener("mouseenter", function (e) {
const tooltip = document.createElement("div");
tooltip.textContent = this.getAttribute("data-title");
tooltip.style.cssText = `
font-family: "VCR", sans-serif;
position: fixed;
background:rgb(255, 255, 255);
color: #000000;
border: 1px solidrgb(0, 0, 0);
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
z-index: 10000;
pointer-events: none;
white-space: nowrap;
`;
document.body.appendChild(tooltip);
this.tooltipEl = tooltip;
const rect = this.getBoundingClientRect();
tooltip.style.left =
rect.left + rect.width / 2 - tooltip.offsetWidth / 2 + "px";
tooltip.style.top = rect.top - tooltip.offsetHeight - 8 + "px";
});
element.addEventListener("mouseleave", function () {
if (this.tooltipEl) {
document.body.removeChild(this.tooltipEl);
this.tooltipEl = null;
}
});
});
}
document.addEventListener("DOMContentLoaded", initDynamicTooltips);
coverArtEl.addEventListener("click", (event) => {
const isNoPause = event.target.classList.contains("no-pause");
if (howlerInstance && !isNoPause) {
togglePlay();
}
});
// uploadInfoEl.addEventListener("click", () => {
// const confirmText = `Do you really want to change your playlist?\nThis will replace all your retrowave music history.\nIf you are sure about this, make sure to upload a valid json file probably downloaded using history link.`;
// if (confirm(confirmText)) {
// fileUploadEl.click();
// }
// });
function listenUploadFileChange() {
fileUploadEl.onchange = function () {
const selectedFile = fileUploadEl.files[0];
const reader = new FileReader();
reader.readAsText(selectedFile, "UTF-8");
reader.onload = function (event) {
try {
const uploadedPlaylist = JSON.parse(event.target.result);
localStorage.setItem("retrowave-history", event.target.result);
} catch (error) {
alert("malformed/invalid json file");
}
};
};
}
document.getElementById("initButton")?.addEventListener("click", async () => {
var hydra = new Hydra({ detectAudio: false });
modalEl.classList.remove("open");
getMusic().then(() => {
playMusic();
});
initHydra();
initControls();
});
document.getElementById("history").addEventListener("click", () => {
downloadHistory();
});
document.addEventListener("keyup", (event) => {
if (!terminalOverlay.classList.contains("hidden")) {
return;
}
const { key } = event;
switch (key) {
case " ":
togglePlay();
break;
case "w":
volumeUp();
break;
case "s":
volumeDown();
break;
case "n":
playNextTrack();
break;
case "f":
toggleFullScreen();
break;
case "h":
toggleControls();
break;
case "x":
toggleEverything();
break;
case "e":
rotateHydraEffect();
break;
case "t":
toggleTerminal();
break;
}
});
let touchstartX = 0;
let touchendX = 0;
let touchstartY = 0;
let touchendY = 0;
const musixPlayerEl = document.getElementsByClassName("music-player")[0];
if (musixPlayerEl) {
musixPlayerEl.addEventListener("touchstart", (e) => {
touchstartX = e.changedTouches[0].screenX;
touchstartY = e.changedTouches[0].screenY;
});
musixPlayerEl.addEventListener("touchend", (e) => {
touchendX = e.changedTouches[0].screenX;
touchendY = e.changedTouches[0].screenY;
checkDirection();
});
}
function checkDirection() {
if (touchendX < touchstartX) {
// Swipe Left
}
if (touchendX > touchstartX) {
// Swipe Right
}
if (touchendY < touchstartY) {
volumeUp();
}
if (touchendY > touchstartY) {
volumeDown();
}
}
function openDialog() {
modalEl.classList.add("open");
}
async function getMusic() {
try {
const res = await fetch(
`https://retrowave.ru/api/v1/tracks?limit=10&cursor=${cursor}`
).then((res) => res.json());
const {
body: { tracks, cursor: currentCursor },
} = res;
cursor = currentCursor;
currentTracks = tracks;
} catch (error) {
console.error("Error fetching music:", error);
showErrors(
"Failed to fetch music. Please check your internet connection or try again later."
);
throw error; // rethrow the error to be caught in the catch block below
}
}
function initPlayer() {}
function playMusic() {
const currentTrack = currentTracks[0];
const singleTrack = currentTrack.streamUrl;
const fullTrack = `${retroWaveRu}${singleTrack}`;
howlerInstance = new Howl({
src: [fullTrack],
html5: true,
onend: function () {
playNextTrack();
},
});
setVolume();
isPlaying = true;
window.hw = howlerInstance;
updateInfo(currentTrack);
addToHistory(currentTrack);
howlerInstance.play();
}
function volumeDown() {
if (volume > 0.1) {
volume -= 0.1;
}
setVolume();
}
function volumeUp() {
if (volume < 1) {
volume += 0.1;
}
setVolume();
}
function setVolume() {
howlerInstance.volume(volume);
}
function togglePlay() {
isPlaying = !isPlaying;
if (isPlaying) {
howlerInstance.play();
} else {
howlerInstance.pause();
}
}
function updateInfo(trackDetails) {
const trackTitle = trackDetails.title.toString().toUpperCase() || "Unknown Track";
titleEl.innerText = trackTitle;
document.title = `Now Playing... ${trackTitle}`;
coverArtEl.style.backgroundImage = `url("${retroWaveRu}${trackDetails.artworkUrl}")`;
ovr.innerHTML = `> ${trackTitle}`;
}
function getHistory() {
let localHistoryStore = localStorage.getItem("retrowave-history") || "[]";
let historyArray = JSON.parse(localHistoryStore);
return historyArray;
}
function addToHistory(trackDetails) {
let historyArray = getHistory();
historyArray.push(trackDetails);
localStorage.setItem("retrowave-history", JSON.stringify(historyArray));
}
function downloadHistory() {
const historyArray = getHistory();
let element = document.createElement("a");
let playListData = "#EXTM3U";
historyArray.forEach((musicData) => {
playListData = `${playListData}
#EXTINF:${Math.ceil(musicData.duration / 1000)}, ${musicData.title}
https://retrowave.ru/${musicData.streamUrl}
`;
});
element.setAttribute(
"href",
"data:audio/x-mpegurl;;charset=utf-8," + encodeURIComponent(playListData)
);
element.setAttribute("download", "retrowave_playlist.m3u");
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function playNextTrack() {
resetHowler();
currentTracks.shift();
if (currentTracks.length <= 3) {
getMusic();
}
playMusic();
}
function resetHowler(destroy = false) {
howlerInstance.stop();
if (destroy) {
howlerInstance.unload();
howlerInstance = null;
}
}
function initControls() {
refreshBtn.addEventListener("click", () => {
playNextTrack();
});
fullScreenBtn.addEventListener("click", () => {
toggleFullScreen();
});
terminalBtn.addEventListener("click", () => {
toggleTerminal();
});
terminalClose.addEventListener("click", () => {
closeTerminal();
});
terminalInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
executeCommand(terminalInput.value.trim());
terminalInput.value = "";
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !terminalOverlay.classList.contains("hidden")) {
closeTerminal();
}
});
}
async function toggleFullScreen() {
if (!document.fullscreenElement) {
try {
await document?.documentElement?.requestFullscreen();
} catch (error) {
console.error("Error attempting to enable full-screen mode:", error);
showErrors(
"Failed to enter full-screen mode. Please check your browser settings."
);
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
function toggleControls() {
const toggleableElements = document.querySelectorAll(".toggleable");
toggleableElements.forEach((el) => {
el.classList.toggle("hidden");
});
}
function toggleEverything() {
musixPlayerEl.classList.toggle("hidden");
ovr.classList.toggle("hidden");
}
function showErrors(error) {
errorEl.classList.remove("hidden");
errorEl.innerText = error;
setTimeout(() => {
errorEl.innerText = "";
errorEl.classList.add("hidden");
}, 10000);
}
// function initCodef() {
// const width = window.innerWidth;
// const height = window.innerHeight;
// effectCanvas = new canvas(width, height, 'codef-canvas');
// starField = new starfield3D(effectCanvas, 500, 2, width, height, width/2, height/2, '#ffffff', 100, 0, 0);
// line3D = new codef3D(effectCanvas, 320, 75, 1, 1500 );
// line3D.line({x:-320, y:0, z:0},{x:320, y:0, z:0}, new LineBasicMaterial({ color: synthwaveColor, linewidth:2}));
// line3D.line({x: 0, y:-240, z:0},{x:0, y:240, z:0}, new LineBasicMaterial({ color: synthwaveColor, linewidth:2}));
// renderCodeFx();
// }
// function renderCodeFx() {
// effectCanvas.fill('#000000');
// line3D.group.rotation.x+=0.01;
// line3D.group.rotation.y+=0.02;
// line3D.group.rotation.z+=0.04;
// starField.draw();
// line3D.draw();
// requestAnimationFrame(renderCodeFx);
// }
const oscRotate = () => {
solid(0, 0).out();
setTimeout(() => {
osc(10).rotate(0.5).diff(osc(200)).out();
});
};
const rainbowWebcam = () => {
solid(0, 0).out();
setTimeout(() => {
s0.initCam();
src(s0).out(o0);
osc(10, 0.2, 0.8).diff(o0).out(o1);
render(o1);
});
};
const waveyzz = () => {
solid(0, 0).out();
setTimeout(() => {
osc(60, -0.015, 0.3)
.diff(osc(60, 0.08).rotate(Math.PI / 2))
.modulateScale(
noise(3.5, 0.25).modulateScale(
osc(15).rotate(() => Math.sin(time / 2))
),
0.6
)
.color(1, 0.5, 0.4)
.contrast(1.4)
.add(src(o0).modulate(o0, 0.04), 0.6)
.invert()
.brightness(0.1)
.contrast(1.2)
.modulateScale(osc(2), -0.2)
.out();
});
};
const vernoi = () => {
setTimeout(() => {
solid(0, 0).out();
voronoi(350, 0.15)
.modulateScale(osc(8).rotate(Math.sin(time)), 0.5)
.thresh(0.8)
.modulateRotate(osc(7), 0.4)
.thresh(0.7)
.diff(src(o0).scale(1.8))
.modulateScale(osc(2).modulateRotate(o0, 0.74))
.diff(
src(o0)
.rotate([-0.012, 0.01, -0.002, 0])
.scrollY(0, [-1 / 199800, 0].fast(0.7))
)
.brightness([-0.02, -0.17].smooth().fast(0.5)) //.modulate(o0, () => a.fft[1] * .2)
.out();
});
};
const hydraEffects = [vernoi, waveyzz, oscRotate];
function rotateHydraEffect() {
try {
hydraEffects[currentEffectIndex]();
currentEffectIndex = (currentEffectIndex + 1) % hydraEffects.length;
} catch (error) {
console.error("Error applying random Hydra effect:", error);
showErrors(
"Failed to apply random Hydra effect. Please check your browser compatibility or try again later."
);
}
}
function initHydra() {
try {
hydraEffects[0]();
} catch (error) {
console.error("Hydra initialization failed:", error);
showErrors(
"Hydra initialization failed. Please check your browser compatibility or try again later."
);
}
}
function toggleTerminal() {
terminalOverlay.classList.toggle("hidden");
if (!terminalOverlay.classList.contains("hidden")) {
terminalInput.focus();
}
}
function closeTerminal() {
terminalOverlay.classList.add("hidden");
}
function addTerminalLine(text, isCommand = false) {
const line = document.createElement("div");
line.className = "terminal-line";
if (isCommand) {
line.innerHTML = `indrajith@retrowave:$ ${text}`;
} else {
line.textContent = text;
}
terminalOutput.appendChild(line);
terminalOutput.scrollTop = terminalOutput.scrollHeight;
}
function executeCommand(command) {
if (!command) return;
addTerminalLine(command, true);
const args = command.toLowerCase().split(' ');
const cmd = args[0];
switch (cmd) {
case 'help':
addTerminalLine('Available commands:');
addTerminalLine(' help - Show this help message');
addTerminalLine(' play - Start/resume playback');
addTerminalLine(' pause - Pause playback');
addTerminalLine(' next - Skip to next track');
addTerminalLine(' volume [0-10] - Set volume (0-10)');
addTerminalLine(' status - Show current track info');
addTerminalLine(' history - Download playlist history');
addTerminalLine(' effect [list|name] - Change/list visual effects');
addTerminalLine(' fullscreen - Toggle fullscreen mode');
addTerminalLine(' clear - Clear terminal');
addTerminalLine(' exit - Close terminal');
break;
case 'play':
if (howlerInstance && !isPlaying) {
togglePlay();
addTerminalLine('Playback resumed.');
} else if (isPlaying) {
addTerminalLine('Already playing.');
} else {
addTerminalLine('No track loaded.');
}
break;
case 'pause':
if (howlerInstance && isPlaying) {
togglePlay();
addTerminalLine('Playback paused.');
} else {
addTerminalLine('Not currently playing.');
}
break;
case 'next':
if (howlerInstance) {
playNextTrack();
addTerminalLine('Skipped to next track.');
} else {
addTerminalLine('No track loaded.');
}
break;
case 'volume':
if (args[1]) {
const vol = parseInt(args[1]);
if (vol >= 0 && vol <= 10) {
volume = vol / 10;
if (howlerInstance) setVolume();
addTerminalLine(`Volume set to ${vol}/10`);
} else {
addTerminalLine('Volume must be between 0 and 10.');
}
} else {
addTerminalLine(`Current volume: ${Math.round(volume * 10)}/10`);
}
break;
case 'status':
if (currentTracks.length > 0) {
const track = currentTracks[0];
addTerminalLine(`Now playing: ${track.title}`);
addTerminalLine(`Status: ${isPlaying ? 'Playing' : 'Paused'}`);
addTerminalLine(`Volume: ${Math.round(volume * 10)}/10`);
} else {
addTerminalLine('No track loaded.');
}
break;
case 'history':
downloadHistory();
addTerminalLine('Playlist history downloaded.');
break;
case 'effect':
if (args[1]) {
if (args[1] === 'list') {
addTerminalLine('Available effects:');
addTerminalLine(' vernoi - Voronoi pattern effect');
addTerminalLine(' waveyzz - Wave synthesis effect');
addTerminalLine(' oscrotate - Oscillating rotation effect');
addTerminalLine('Usage: effect or effect list');
} else {
const effectName = args[1].toLowerCase();
let effectIndex = -1;
switch (effectName) {
case 'vernoi':
effectIndex = 0;
break;
case 'waveyzz':
effectIndex = 1;
break;
case 'oscrotate':
effectIndex = 2;
break;
default:
addTerminalLine(`Unknown effect: ${effectName}`);
addTerminalLine('Type "effect list" to see available effects.');
break;
}
if (effectIndex !== -1) {
try {
hydraEffects[effectIndex]();
currentEffectIndex = effectIndex;
addTerminalLine(`Effect changed to: ${effectName}`);
} catch (error) {
addTerminalLine('Failed to apply effect.');
}
}
}
} else {
rotateHydraEffect();
addTerminalLine('Visualization changed to next effect.');
}
break;
case 'fullscreen':
toggleFullScreen();
addTerminalLine('Toggled fullscreen mode.');
break;
case 'clear':
terminalOutput.innerHTML = '';
break;
case 'exit':
closeTerminal();
break;
default:
addTerminalLine(`Command not found: ${cmd}`);
addTerminalLine('Type "help" for available commands.');
break;
}
addTerminalLine('');
}
})();