(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;
}
}
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 = () => {
setTimeout(() => {
osc(15, 0.02, 0.8)
.color(1.0, 0.16, 0.46) // Neon pink
.rotate(0.3)
.add(
osc(30, 0.01, 0.6)
.color(0.0, 1.0, 1.0) // Cyan
.rotate(-0.2)
.modulateRotate(osc(2), 0.5),
0.4
)
.diff(
osc(100, 0.005, 0.3)
.color(0.8, 0.2, 1.0) // Purple
.rotate(Math.PI / 3)
)
.modulateScale(osc(1, 0.1), 0.2)
.add(
shape(6, 0.8, 0.01)
.color(0.0, 1.0, 1.0) // Cyan grid
.rotate(() => time * 0.1)
.scale(0.3),
0.1
)
.contrast(1.8)
.brightness(0.1)
.out();
}, 100);
};
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 = () => {
setTimeout(() => {
osc(40, -0.01, 0.4)
.color(1.0, 0.16, 0.46) // Neon pink
.diff(
osc(40, 0.05)
.color(0.0, 1.0, 1.0) // Cyan
.rotate(Math.PI / 2)
)
.modulateScale(
noise(2.5, 0.3).modulateScale(
osc(10).rotate(() => Math.sin(time / 3))
),
0.4
)
.add(
osc(80, 0.02, 0.1)
.color(0.5, 0.0, 1.0) // Purple
.rotate(Math.PI / 4),
0.2
)
.contrast(2.0)
.add(src(o0).modulate(o0, 0.03), 0.7)
.modulateHue(osc(0.5, 0.1), 0.3)
.brightness(0.15)
.contrast(1.6)
.modulateScale(osc(1.5), -0.15)
.out();
}, 100);
};
const vernoi = () => {
setTimeout(() => {
voronoi(25, 0.15)
.color(1.0, 0.16, 0.46) // Neon pink
.add(
voronoi(15, 0.2)
.color(0.0, 1.0, 1.0) // Cyan
.modulateScale(osc(4), 0.2),
0.6
)
.add(
voronoi(35, 0.1)
.color(0.8, 0.2, 1.0) // Purple
.modulateRotate(osc(1), 0.3),
0.4
)
.modulateScale(osc(8), 0.3)
.contrast(1.8)
.brightness(0.15)
.out();
}, 100);
};
const retroSun = () => {
setTimeout(() => {
shape(16, 0.6, 0.02)
.color(1.0, 0.5, 0.0) // Orange/yellow sun
.scale(0.8)
.add(
shape(8, 0.4, 0.05)
.color(1.0, 0.16, 0.46) // Pink inner glow
.scale(0.6),
0.6
)
.add(
osc(20, 0.01, 0.2)
.color(0.0, 1.0, 1.0) // Cyan horizontal lines
.rotate(Math.PI / 2),
0.3
)
.modulateScale(
osc(2, 0.1),
0.1
)
.add(
gradient(1.0)
.color(0.2, 0.0, 0.4) // Dark purple gradient
.rotate(Math.PI)
.scale(2.0),
0.4
)
.contrast(1.8)
.brightness(0.15)
.out();
}, 100);
};
const synthWave = () => {
setTimeout(() => {
// Animated synthwave with moving grid lines and pulsing elements
osc(30, 0, 1)
.thresh(0.9)
.color(0.0, 1.0, 1.0) // Cyan horizontal lines
.scrollY(() => time * 0.5) // Moving grid lines
.add(
gradient(0.5)
.color(1.0, 0.16, 0.46) // Pink to black gradient
.rotate(Math.PI)
.scale(1.5)
.modulateHue(osc(0.3, 0.1), 0.2), // Subtle hue shift
0.7
)
.add(
shape(3, 0.8, 0.1)
.color(1.0, 0.5, 0.0) // Orange triangle/sun shape
.scale(() => 0.3 + Math.sin(time * 2) * 0.05) // Pulsing size
.repeatY(3, 0.3)
.mult(
osc(5, 0.1, 0.8)
.color(0.8, 0.2, 1.0) // Purple modulation
.rotate(() => time * 0.1) // Slow rotation
),
0.4
)
.add(
osc(8, 0.01, 0.2)
.color(0.0, 1.0, 1.0) // More cyan lines
.thresh(0.8)
.scrollY(() => -time * 0.3) // Counter-moving lines
.mult(
gradient(1.0)
.color(0.2, 0.0, 0.4) // Dark purple fade
.scale(2.0)
.modulateScale(osc(1, 0.1), 0.1), // Breathing effect
),
0.3
)
.modulateHue(osc(0.1), 0.1) // Overall subtle color shift
.contrast(2.2)
.brightness(0.1)
.out();
}, 100);
};
const hydraEffects = [vernoi, waveyzz, oscRotate, retroSun, synthWave];
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('');
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 (Desktop only)');
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 - Synthwave Voronoi patterns');
addTerminalLine(' waveyzz - Neon wave synthesis');
addTerminalLine(' oscrotate - Oscillating neon shapes');
addTerminalLine(' retrosun - Retro sunset with grid');
addTerminalLine(' synthwave - Basic synthwave pattern');
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;
case 'retrosun':
effectIndex = 3;
break;
case 'synthwave':
effectIndex = 4;
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('');
}
})();