const readline = require('readline'); readline.emitKeypressEvents(process.stdin); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } const modes = ['classic', 'bottom', 'top']; let mode = 0; let modePopup = 0; const modePopupDuration = 2; process.stdin.on('keypress', (str, key) => { if (key.name === 'space') { mode++; mode = mode % modes.length; modePopup = modePopupDuration; } if (key.sequence === '\x03' || key.sequence === '\x04') process.exit(0); // ctrl+c, ctrl+d }); process.on('exit', () => { process.stdout.write('\x1B[?25h\033[0m'); // show cursor }) const plas = [' ', '.', '*', '/', '0']; const progressChars = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; //const progressChars = ['0', '1', '2', '3', '4', '5', '6', '7']; const refreshrate = 50; const errorDuration = 4; function outCirc(t) {return Math.sqrt(-t * t + 2 * t)} function plasma(x, y, time, k) { let v = 0; let cx = x * k - k / 2; let cy = y * k - k / 2; v += Math.sin(cx + time); v += Math.sin((cy + time) / 2); v += Math.sin((cx + cy + time) / 2); cx = cx + k / 2 * Math.sin(time / 3); cy = cy + k / 2 * Math.cos(time / 2); v += Math.sin(Math.sqrt(cx * cx + cy * cy + 1) + time); v /= 2; return Math.sin(Math.PI * v) * 0.5 + 0.5; } let displaytitle = ''; let displayalbum = ''; // type: 0 = error, 1 = warning, 2 = info let popups = []; function transform(old, n) { if (old.length < n.length) { // old += ' '.repeat(n.length - old.length); old += ' '; } else if (old.length > n.length) { old = old.slice(0, old.length - 1); } let changeable = []; for (let i = 0; i < old.length; i++) { if (old[i] !== n[i] && n[i] && old[i]) changeable.push(i); } let rand = Math.floor(Math.random() * changeable.length); let i = changeable[rand]; if (n[i]) { return old.slice(0, i) + n[i] + old.slice(i + 1); } return old; } let time = 0; function render(artist, album, title, songStart, songEnd, pauseSpot, paused) { let dt = refreshrate / 1000 time += dt; modePopup -= dt; let w = process.stdout.columns; let h = process.stdout.rows; let newtitle = `${artist.toLowerCase()} - ${title.toLowerCase()}`; if (newtitle !== displaytitle) { displaytitle = transform(displaytitle, newtitle); } let newalbum = `album / ${album.toLowerCase()}`; if (newalbum !== displayalbum) { displayalbum = transform(displayalbum, newalbum); } // leaving the coordinates as non-integer values is very undefined behavior let texts = []; switch (mode) { case 0: texts.push({ value: ' ' + displaytitle + ' ', x: Math.round(w / 2 - displaytitle.length / 2 + Math.sin(time) * 5), y: Math.floor(h / 2) - 1, color: '\033[7;1m' }); texts.push({ value: ' ' + displayalbum + ' ', x: Math.round(w / 2 - displayalbum.length / 2 - Math.sin(time) * 5), y: Math.floor(h / 2) + 1, color: '\033[7m' }); break; case 1: texts.push({ value: ' ' + displaytitle + ' '.repeat(w), x: 0, y: h - 3, color: '\033[7;1m' }); texts.push({ value: ' ' + displayalbum + ' '.repeat(w), x: 0, y: h - 2, color: '\033[7m' }); break; case 2: texts.push({ value: ' ' + displaytitle + ' '.repeat(w), x: 0, y: 0, color: '\033[7;1m' }); texts.push({ value: ' ' + displayalbum + ' '.repeat(w), x: 0, y: 1, color: '\033[7m' }); break; } let i = 0; for (let popup of popups) { popup.timer -= dt; if (popup.timer < 0) { popups.splice(i, 1); } else { let symbol = ['⚠️', '⚠️', 'ℹ'][popup.type]; let color = ['\033[41;37m', '\033[43;37m', '\033[44;37m'][popup.type]; let text = ` ${symbol} ${popup.text} `; texts.push({ value: ' '.repeat(text.length) + text, x: -text.length * 2 + Math.round((text.length + 3) * outCirc(Math.min(popup.timer, 1))), y: 3 + i, color: color }); i++; } } if (modePopup > 0) { texts.push({ value: modes[mode], x: Math.floor(w - ((modes[mode].length + 2) * outCirc(Math.min(modePopup, 1)))), y: 0, color: '\033[40;37m' }); } let reset = '\033[0m'; let now = paused ? pauseSpot : Date.now(); let timeCurrent = now - songStart; let timeAll = songEnd - songStart; let timerCurrent = String(Math.floor(timeCurrent / 60000)).padStart(2, '0') + ':' + String(Math.floor((timeCurrent % 60000) / 1000)).padStart(2, '0'); let timerAll = String(Math.floor(timeAll / 60000)).padStart(2, '0') + ':' + String(Math.floor((timeAll % 60000) / 1000)).padStart(2, '0'); timerString = timerCurrent + ' / ' + timerAll; let progress = (now - songStart) / (songEnd - songStart); for (let y = 0; y < h; y++) { let renderingtext = false; let renderingtextLast = false; readline.cursorTo(process.stdout, 0, y); process.stdout.write('\x1B[?25l\033[0m'); // hide cursor, reset color for (let x = 0; x < w; x++) { let sum = ''; let isText = false; let text; for (let t of texts) { if (y === t.y && x >= t.x && x < t.x + t.value.length) { isText = true; text = t; } } renderingtext = isText; if (renderingtext && !renderingtextLast) { sum += text.color; } else if (!renderingtext && renderingtextLast) { sum += reset; } if (text) { sum += text.value[x - text.x] || ' '; } else { if (y === h - 1) { let blockProgress = (progress % (1/w)) / (1/w); let blocks = progress * w; let inProgressArea = (x > Math.floor(blocks) - 0.5) && (x < Math.ceil(blocks) - 0.5); let pChar = progressChars[Math.floor(blockProgress * progressChars.length)]; let color = (x / (w - 1) > (Math.floor(progress * w) / w)) ? '\033[40;37m' : '\033[47;30m'; let pause = (paused && x === (w - 2)) ? '⏸' : null; sum += color + (pause || (timerString[x - 1] === ' ' ? null : timerString[x - 1]) || (inProgressArea ? pChar : ' ')) + '\033[0m'; } else if (x === 0 || x === w - 1 || y === 0) { sum += '\033[40m \033[0m'; } else { let gate = 2.5 let l1 = plasma(x, y * 2, time, 4 / Math.min(w, h)); let l2 = Math.max(plasma(x, y * 2, time * 0.7, 3 / Math.min(w, h)) * gate - (gate - 1), 0); sum += ((l2 > (1 / plas.length)) ? '\x1B[90m' + plas[Math.floor(l2 * plas.length)] : '\033[30m' + plas[Math.floor(l1 * plas.length)]) + '\033[0m'; //sum += '\033[30m' + plas[Math.floor(plasma(x, y * 2, time, 4 / Math.min(w, h)) * plas.length)] + '\033[0m'; } } renderingtextLast = renderingtext; process.stdout.write(sum); } } } function error(content) {popups.push({text: content, timer: errorDuration, type: 0});} function warning(content) {popups.push({text: content, timer: errorDuration, type: 1});} function info(content) {popups.push({text: content, timer: errorDuration, type: 2});} module.exports.render = render; module.exports.refreshrate = refreshrate; module.exports.error = error; module.exports.warning = warning; module.exports.info = info;